Skip to content

Commit 7fb3fb7

Browse files
Support for both single and high/low tariff #130
1 parent 27c28cc commit 7fb3fb7

File tree

20 files changed

+275
-107
lines changed

20 files changed

+275
-107
lines changed

dsmr_backend/mixins.py

+14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from django.utils.translation import ugettext as _
77
from django.conf import settings
8+
from django.contrib import admin
89

910

1011
class InfiniteManagementCommandMixin(object):
@@ -73,3 +74,16 @@ def _signal_handler(self, signum, frame):
7374
# If we get called, then we must gracefully exit.
7475
self._keep_alive = False
7576
self.stdout.write('Detected signal #{}, exiting on next run...'.format(signum))
77+
78+
79+
class ReadOnlyAdminModel(admin.ModelAdmin):
80+
""" Read only model for Django admin. """
81+
def __init__(self, *args, **kwargs):
82+
super(ReadOnlyAdminModel, self).__init__(*args, **kwargs)
83+
self.readonly_fields = self.model._meta.get_all_field_names()
84+
85+
def has_add_permission(self, request, obj=None):
86+
return False
87+
88+
def has_delete_permission(self, request, obj=None):
89+
return False

dsmr_backup/tests/test_services.py

+32
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,32 @@ def test_create(self, compress_mock, subprocess_mock):
101101
self.assertFalse(compress_mock.called)
102102
self.assertFalse(subprocess_mock.called)
103103
self.assertIsNone(BackupSettings.get_solo().latest_backup)
104+
self.assertTrue(BackupSettings.get_solo().compress)
104105

105106
dsmr_backup.services.backup.create()
106107
self.assertTrue(compress_mock.called)
107108
self.assertTrue(subprocess_mock.called)
108109

109110
self.assertIsNotNone(BackupSettings.get_solo().latest_backup)
110111

112+
@mock.patch('subprocess.Popen')
113+
@mock.patch('dsmr_backup.services.backup.compress')
114+
def test_create_without_compress(self, compress_mock, subprocess_mock):
115+
backup_settings = BackupSettings.get_solo()
116+
backup_settings.compress = False
117+
backup_settings.save()
118+
119+
self.assertFalse(compress_mock.called)
120+
self.assertFalse(subprocess_mock.called)
121+
self.assertIsNone(BackupSettings.get_solo().latest_backup)
122+
self.assertFalse(BackupSettings.get_solo().compress)
123+
124+
dsmr_backup.services.backup.create()
125+
self.assertFalse(compress_mock.called)
126+
self.assertTrue(subprocess_mock.called)
127+
128+
self.assertIsNotNone(BackupSettings.get_solo().latest_backup)
129+
111130
def test_compress(self):
112131
TEST_STRING = b'TestTestTest-1234567890'
113132
# Temp file without automtic deletion, as compress() should do that as well.
@@ -157,6 +176,19 @@ def test_sync_disabled(self, upload_chunked_mock):
157176
dsmr_backup.services.dropbox.sync()
158177
self.assertFalse(upload_chunked_mock.called)
159178

179+
@mock.patch('dsmr_backup.services.dropbox.upload_chunked')
180+
@mock.patch('django.utils.timezone.now')
181+
def test_sync_initial(self, now_mock, upload_chunked_mock):
182+
now_mock.return_value = timezone.make_aware(timezone.datetime(2016, 1, 1))
183+
184+
# Initial project state.
185+
dropbox_settings = DropboxSettings.get_solo()
186+
dropbox_settings.latest_sync = None
187+
dropbox_settings.save()
188+
189+
dsmr_backup.services.dropbox.sync()
190+
self.assertTrue(upload_chunked_mock.called)
191+
160192
@mock.patch('dsmr_backup.services.dropbox.upload_chunked')
161193
@mock.patch('django.utils.timezone.now')
162194
def test_sync(self, now_mock, upload_chunked_mock):

dsmr_datalogger/admin.py

+15
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
from django.contrib import admin
22
from solo.admin import SingletonModelAdmin
33

4+
from dsmr_backend.mixins import ReadOnlyAdminModel
45
from .models.settings import DataloggerSettings
6+
from .models.reading import DsmrReading, MeterStatistics
57

68

79
@admin.register(DataloggerSettings)
810
class DataloggerSettingsAdmin(SingletonModelAdmin):
911
list_display = ('com_port', )
12+
13+
14+
@admin.register(DsmrReading)
15+
class DsmrReadingAdmin(ReadOnlyAdminModel):
16+
""" Read only model. """
17+
ordering = ['-timestamp']
18+
list_display = ('timestamp', 'electricity_currently_delivered', 'electricity_currently_returned')
19+
20+
21+
@admin.register(MeterStatistics)
22+
class MeterStatisticsAdmin(ReadOnlyAdminModel):
23+
""" Read only model. """
24+
list_display = ('timestamp', 'electricity_tariff', 'power_failure_count')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('dsmr_datalogger', '0001_squashed_0005_optional_gas_readings'),
11+
]
12+
13+
operations = [
14+
migrations.AlterModelOptions(
15+
name='dsmrreading',
16+
options={'verbose_name': 'DSMR reading (read only)', 'default_permissions': (), 'ordering': ['timestamp'], 'verbose_name_plural': 'DSMR readings (read only)'},
17+
),
18+
migrations.AlterModelOptions(
19+
name='meterstatistics',
20+
options={'verbose_name': 'DSMR Meter statistics (read only)', 'default_permissions': (), 'verbose_name_plural': 'DSMR Meter statistics (read only)'},
21+
),
22+
]

dsmr_datalogger/models/reading.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ class DsmrReading(models.Model):
7171
class Meta:
7272
default_permissions = tuple()
7373
ordering = ['timestamp']
74-
verbose_name = _('DSMR reading')
74+
verbose_name = _('DSMR reading (read only)')
75+
verbose_name_plural = _('DSMR readings (read only)')
7576

7677
def __str__(self):
7778
return '{}: {} kWh'.format(self.id, self.timestamp, self.electricity_currently_delivered)
@@ -135,7 +136,8 @@ class MeterStatistics(SingletonModel):
135136

136137
class Meta:
137138
default_permissions = tuple()
138-
verbose_name = _('DSMR Meter statistics')
139+
verbose_name = _('DSMR Meter statistics (read only)')
140+
verbose_name_plural = verbose_name
139141

140142
def __str__(self):
141143
return '{} @ {}'.format(self.__class__.__name__, self.timestamp)

dsmr_datalogger/services.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def _convert_legacy_dsmr_gas_line(parsed_reading, current_line, next_line):
106106
""" Legacy support for DSMR 2.x gas. """
107107
legacy_gas_line = current_line
108108

109-
if next_line.startswith('('):
109+
if next_line.startswith('('): # pragma: no cover
110110
legacy_gas_line = current_line + next_line
111111

112112
legacy_gas_result = re.search(

dsmr_frontend/templates/dsmr_frontend/dashboard.html

+39-32
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,29 @@
88
{% block header %}{% trans "Dashboard" %} &nbsp; <i class="fa fa-dashboard"></i>{% endblock %}
99

1010
{% block header_right %}
11-
{% if capabilities.electricity and latest_electricity_read %}
1211
<li>
1312
<a href="" onclick="return false;" style="cursor: default;">
14-
<span class="badge bg-red"><i class="fa fa-bolt"></i> &nbsp; {{ latest_electricity }} &nbsp; {% trans "Watt" %}</span>
15-
<small style="color: #999999;">({{ latest_electricity_read|naturaltime }})</small>
13+
<small style="color: #999999;"><span id="latest_timestamp"></span></small>
1614
</a>
1715
</li>
18-
{% endif %}
1916

20-
{% if capabilities.electricity_returned and latest_electricity_returned %}
21-
<li>
22-
<a href="" onclick="return false;" style="cursor: default;">
23-
<span class="badge bg-green"><i class="fa fa-bolt"></i> &nbsp; {{ latest_electricity_returned }} &nbsp; {% trans "Watt" %}</span>
24-
<small style="color: #999999;">({{ latest_electricity_read|naturaltime }})</small>
25-
</a>
26-
</li>
27-
{% endif %}
28-
29-
{% if capabilities.gas and latest_gas_read %}
30-
<li>
31-
<a href="" onclick="return false;" style="cursor: default;">
32-
<span class="badge bg-orange"><i class="fa fa-fire"></i> &nbsp; {{ latest_gas }} &nbsp; {% trans "m<sup>3</sup>" %}</span>
33-
<small style="color: #999999;">({{ latest_gas_read|naturaltime }})</small>
34-
</a>
35-
</li>
36-
{% endif %}
37-
38-
{% if latest_temperature %}
39-
<li>
40-
<a href="" onclick="return false;" style="cursor: default;">
41-
<span class="badge bg-blue"><i class="fa fa-cloud"></i> &nbsp; {{ latest_temperature }} &nbsp; &deg;C</span>
42-
<small style="color: #999999;">({{ latest_temperature_read|naturaltime }})</small>
43-
</a>
44-
</li>
45-
{% endif %}
17+
{% if capabilities.electricity %}
18+
<li>
19+
<a href="" onclick="return false;" style="cursor: default;">
20+
<span class="badge bg-red"><i class="fa fa-bolt"></i>
21+
&nbsp; <span id="latest_electricity"><i class="fa fa-refresh fa-spin fa-fw"></i></span> &nbsp; {% trans "Watt" %}</span>
22+
</a>
23+
</li>
24+
{% endif %}
25+
26+
{% if capabilities.electricity_returned %}
27+
<li>
28+
<a href="" onclick="return false;" style="cursor: default;">
29+
<span class="badge bg-green"><i class="fa fa-bolt"></i>
30+
&nbsp; <span id="latest_electricity_returned"><i class="fa fa-refresh fa-spin fa-fw"></i></span> &nbsp; {% trans "Watt" %}</span>
31+
</a>
32+
</li>
33+
{% endif %}
4634
{% endblock %}
4735

4836
{% block content %}
@@ -280,10 +268,29 @@
280268
render_temperature_chart();
281269
{% endif %}
282270

283-
/* Auto reload page. */
284-
setInterval(function(){ location.reload(); }, 10000);
271+
/* Auto refresh. */
272+
update_header();
273+
setInterval(function(){ update_header(); }, 5000);
285274
});
286275

276+
/**
277+
* Updates the header.
278+
*/
279+
function update_header()
280+
{
281+
$("#header-loader").show();
282+
283+
$.ajax({
284+
dataType: "json",
285+
url: "{% url 'frontend:dashboard-xhr-header' %}",
286+
}).done(function(response) {
287+
$("#header-loader").hide();
288+
$("#latest_timestamp").html(response.timestamp);
289+
$("#latest_electricity").html(response.currently_delivered);
290+
$("#latest_electricity_returned").html(response.currently_returned);
291+
});
292+
}
293+
287294
{% if capabilities.electricity %}
288295
function render_electricity_chart()
289296
{

dsmr_frontend/templates/dsmr_frontend/statistics.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,15 @@
166166
<th class="col-md-6">{% trans "Unit price" %} (&euro;)</th>
167167
</tr>
168168
<tr>
169-
<td class="col-md-6">{% trans "Electricity 1 (per kWh)" %}</td>
169+
<td class="col-md-6">{% trans "Electricity 1 price (low tariff)" %}</td>
170170
<td class="col-md-6"><span class="badge bg-black">{{ energy_prices.electricity_1_price }}</span></td>
171171
</tr>
172172
<tr>
173-
<td>{% trans "Electricity 2 (per kWh)" %}</td>
173+
<td>{% trans "Electricity 2 price (high tariff)" %}</td>
174174
<td><span class="badge bg-black">{{ energy_prices.electricity_2_price }}</span></td>
175175
</tr>
176176
<tr>
177-
<td>{% trans "Gas (per m<sup>3</sup>)" %}</td>
177+
<td>{% trans "Gas price" %}</td>
178178
<td><span class="badge bg-black">{{ energy_prices.gas_price }}</span></td>
179179
</tr>
180180
</table>

dsmr_frontend/tests/test_webinterface.py

+53-15
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from dsmr_datalogger.models.reading import DsmrReading
2020
import dsmr_consumption.services
2121
from dsmr_frontend.forms import ExportAsCsvForm
22+
from dsmr_frontend.models.message import Notification
2223

2324

2425
class TestViews(TestCase):
@@ -88,14 +89,12 @@ def test_dashboard(self, now_mock):
8889
len(json.loads(response.context['electricity_y'])), 0
8990
)
9091

91-
self.assertGreater(response.context['latest_electricity'], 0)
9292
self.assertTrue(response.context['track_temperature'])
9393
self.assertIn('consumption', response.context)
9494

9595
if self.support_gas:
9696
self.assertGreater(len(json.loads(response.context['gas_x'])), 0)
9797
self.assertGreater(len(json.loads(response.context['gas_y'])), 0)
98-
self.assertEqual(response.context['latest_gas'], 0)
9998

10099
# Test whether reverse graphs work.
101100
frontend_settings = FrontendSettings.get_solo()
@@ -105,6 +104,13 @@ def test_dashboard(self, now_mock):
105104
response = self.client.get(
106105
reverse('{}:dashboard'.format(self.namespace))
107106
)
107+
self.assertEqual(response.status_code, 200)
108+
109+
# XHR views.
110+
response = self.client.get(
111+
reverse('{}:dashboard-xhr-header'.format(self.namespace))
112+
)
113+
self.assertEqual(response.status_code, 200, response.content)
108114

109115
@mock.patch('django.utils.timezone.now')
110116
def test_archive(self, now_mock):
@@ -120,16 +126,22 @@ def test_archive(self, now_mock):
120126
'date': formats.date_format(timezone.now().date(), 'DSMR_DATEPICKER_DATE_FORMAT'),
121127
}
122128
for current_level in ('days', 'months', 'years'):
123-
data.update({'level': current_level})
124-
response = self.client.get(
125-
reverse('{}:archive-xhr-summary'.format(self.namespace)), data=data
126-
)
127-
self.assertEqual(response.status_code, 200)
128-
129-
response = self.client.get(
130-
reverse('{}:archive-xhr-graphs'.format(self.namespace)), data=data
131-
)
132-
self.assertEqual(response.status_code, 200, response.content)
129+
# Test both with tariffs sparated and merged.
130+
for merge in (False, True):
131+
frontend_settings = FrontendSettings.get_solo()
132+
frontend_settings.merge_electricity_tariffs = merge
133+
frontend_settings.save()
134+
135+
data.update({'level': current_level})
136+
response = self.client.get(
137+
reverse('{}:archive-xhr-summary'.format(self.namespace)), data=data
138+
)
139+
self.assertEqual(response.status_code, 200)
140+
141+
response = self.client.get(
142+
reverse('{}:archive-xhr-graphs'.format(self.namespace)), data=data
143+
)
144+
self.assertEqual(response.status_code, 200, response.content)
133145

134146
# Invalid XHR.
135147
data.update({'level': 'INVALID DATA'})
@@ -324,15 +336,41 @@ def test_configuration_force_backup(self, now_mock):
324336

325337
success_url = reverse('{}:configuration'.format(self.namespace))
326338
self.assertEqual(response.status_code, 302)
327-
self.assertEqual(
328-
response['Location'], 'http://testserver{}'.format(success_url)
329-
)
339+
self.assertEqual(response['Location'], 'http://testserver{}'.format(success_url))
340+
330341
# Setting should have been altered.
331342
self.assertEqual(
332343
BackupSettings.get_solo().latest_backup,
333344
now_mock.return_value - timezone.timedelta(days=7)
334345
)
335346

347+
def test_notification_read(self):
348+
view_url = reverse('{}:notification-read'.format(self.namespace))
349+
notification = Notification.objects.create(message='TEST', redirect_to='fake')
350+
self.assertFalse(notification.read)
351+
352+
# Check login required.
353+
response = self.client.post(view_url)
354+
self.assertEqual(response.status_code, 302)
355+
self.assertEqual(
356+
response['Location'], 'http://testserver/admin/login/?next={}'.format(view_url)
357+
)
358+
359+
# Login and retest.
360+
self.client.login(username='testuser', password='passwd')
361+
response = self.client.post(view_url)
362+
363+
response = self.client.post(view_url, data={'id': notification.pk})
364+
self.assertEqual(response.status_code, 302)
365+
366+
# Notification should be altered now.
367+
self.assertTrue(Notification.objects.get(pk=notification.pk).read)
368+
369+
def test_docs_redirect(self):
370+
response = self.client.get(reverse('{}:docs'.format(self.namespace)))
371+
self.assertEqual(response.status_code, 302)
372+
self.assertTrue(response['Location'].startswith('https://dsmr-reader.readthedocs.io'))
373+
336374

337375
class TestViewsWithoutData(TestViews):
338376
""" Same tests as above, but without any data as it's flushed in setUp(). """

dsmr_frontend/urls.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.conf import settings
44
from django.conf.urls import url
55

6-
from dsmr_frontend.views.dashboard import Dashboard
6+
from dsmr_frontend.views.dashboard import Dashboard, DashboardXhrHeader
77
from dsmr_frontend.views.archive import Archive, ArchiveXhrSummary, ArchiveXhrGraphs
88
from dsmr_frontend.views.statistics import Statistics
99
from dsmr_frontend.views.trends import Trends
@@ -18,6 +18,7 @@
1818
urlpatterns = [
1919
# Public views.
2020
url(r'^$', Dashboard.as_view(), name='dashboard'),
21+
url(r'^xhr/header$', DashboardXhrHeader.as_view(), name='dashboard-xhr-header'),
2122
url(r'^archive$', Archive.as_view(), name='archive'),
2223
url(r'^archive/xhr/summary$', ArchiveXhrSummary.as_view(), name='archive-xhr-summary'),
2324
url(r'^archive/xhr/graphs$', ArchiveXhrGraphs.as_view(), name='archive-xhr-graphs'),

0 commit comments

Comments
 (0)