Skip to content

Commit

Permalink
Release v1.14
Browse files Browse the repository at this point in the history
  • Loading branch information
dennissiemensma committed Mar 11, 2018
1 parent 4003e9d commit 881b0e8
Show file tree
Hide file tree
Showing 30 changed files with 1,054 additions and 161 deletions.
2 changes: 1 addition & 1 deletion docs/application.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Public webinterface warning

- Install a firewall, such as ``ufw`` `UncomplicatedFirewall <https://wiki.ubuntu.com/UncomplicatedFirewall>`_ and restrict traffic to port ``22`` (only for yourself) and port ``80``.

- You should also have Nginx restrict application access when exposing it to the Internet. Simply generate an htpasswd string `using one of the many generators found online <https://www.transip.nl/htpasswd/>`_.
- You should also have Nginx restrict application access when exposing it to the Internet. Simply generate an htpasswd string `using one of the many generators found online <http://www.htaccesstools.com/htpasswd-generator/>`_.

- Paste the htpasswd string in ``/etc/nginx/htpasswd``.

Expand Down
14 changes: 14 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ Please make sure you have a fresh **database backup** before upgrading! Upgradin



v1.14.0 - 2018-03-11
^^^^^^^^^^^^^^^^^^^^

**Tickets resolved in this release:**

- [`#441 <https://github.com/dennissiemensma/dsmr-reader/issues/441>`_] PVOutput exports schedulen naar ingestelde upload interval - by pyrocumulus
- [`#436 <https://github.com/dennissiemensma/dsmr-reader/issues/436>`_] Update docs: authentication method for public webinterface
- [`#445 <https://github.com/dennissiemensma/dsmr-reader/issues/445>`_] Upload/export to PVoutput doesn't work
- [`#432 <https://github.com/dennissiemensma/dsmr-reader/issues/432>`_] [API] Gas cost missing at start of day
- [`#367 <https://github.com/dennissiemensma/dsmr-reader/issues/367>`_] Dagverbruik en teruglevering via MQTT
- [`#447 <https://github.com/dennissiemensma/dsmr-reader/issues/447>`_] Kosten via MQTT



v1.13.2 - 2018-02-02
^^^^^^^^^^^^^^^^^^^^

Expand Down
10 changes: 9 additions & 1 deletion dsmr_api/tests/v2/test_consumption.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,21 @@ def test_get(self, now_mock):

FIELDS = (
'day', 'electricity1', 'electricity2', 'electricity1_returned', 'electricity2_returned',
'electricity1_cost', 'electricity2_cost', 'total_cost'
'electricity1_cost', 'electricity2_cost', 'gas', 'gas_cost', 'total_cost'
)

for x in FIELDS:
self.assertIn(x, result.keys())


class TestTodayWithGas(TestToday):
fixtures = [
'dsmr_api/test_electricity_consumption.json',
'dsmr_api/test_electricity_consumption.json',
'dsmr_api/test_gas_consumption.json'
]


class TestElectricity(APIv2TestCase):
fixtures = ['dsmr_api/test_electricity_consumption.json']

Expand Down
8 changes: 8 additions & 0 deletions dsmr_api/views/v2.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from decimal import Decimal

from rest_framework import mixins, viewsets
from rest_framework.response import Response
from rest_framework.views import APIView
Expand Down Expand Up @@ -31,6 +33,7 @@ class TodayConsumptionView(APIView):
'electricity2_returned_end', 'electricity_cost_merged', 'electricity_merged', 'electricity_returned_merged',
'average_temperature', 'lowest_temperature', 'highest_temperature', 'latest_consumption'
)
DEFAULT_ZERO_FIELDS = ('gas', 'gas_cost') # These might miss during the first hour of each day.

def get(self, request):
try:
Expand All @@ -45,6 +48,11 @@ def get(self, request):
if x in day_totals.keys():
del day_totals[x]

# Default these, if omitted.
for x in self.DEFAULT_ZERO_FIELDS:
if x not in day_totals.keys():
day_totals[x] = Decimal(0)

return Response(day_totals)


Expand Down
7 changes: 0 additions & 7 deletions dsmr_backend/management/commands/dsmr_backend.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import traceback

from raven.contrib.django.raven_compat.models import client as raven_client
from django.core.management.base import BaseCommand
from django.utils.translation import ugettext as _
from django.utils import timezone
Expand All @@ -25,12 +24,6 @@ def run(self, **options):

for current_receiver, current_response in responses:
if isinstance(current_response, Exception):
try:
# Raven should capture each exception encountered (below).
raise current_response
except Exception:
raven_client.captureException()

# Add and print traceback to help debugging any issues raised.
exception_traceback = traceback.format_tb(current_response.__traceback__, limit=100)
exception_traceback = "\n".join(exception_traceback)
Expand Down
5 changes: 4 additions & 1 deletion dsmr_backend/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ def handle(self, **options):
print('Starting infinite command loop...') # Just to make sure it gets printed.

while self._keep_alive:
self.run(**options)
try:
self.run(**options)
except Exception as error:
self.stdout.write(' [!] Exception raised in run(): {}'.format(error))

if self.sleep_time is not None:
self.stdout.write('Command completed. Sleeping for {} second(s)...'.format(self.sleep_time))
Expand Down
13 changes: 6 additions & 7 deletions dsmr_backend/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,17 @@ def _fake_signal_troublemaker(*args, **kwargs):
# We must disconnect to prevent other tests from failing, since this is no database action.
dsmr_backend.signals.backend_called.disconnect(receiver=_fake_signal_troublemaker)

@mock.patch('raven.contrib.django.raven_compat.models.client.captureException')
def test_raven_handler(self, raven_mock):
""" Test whether Raven gets called as expectedly, sending any exceptions to Sentry. """

@mock.patch('traceback.format_tb')
def test_signal_exception_handling(self, format_tb_mock):
""" Tests signal exception handling. """
def _fake_signal_troublemaker(*args, **kwargs):
raise AssertionError("Please report me to Raven as I'm a very annoying crash!")
raise AssertionError("Crash")

dsmr_backend.signals.backend_called.connect(receiver=_fake_signal_troublemaker)
self.assertFalse(raven_mock.called)
self.assertFalse(format_tb_mock.called)

self._intercept_command_stdout('dsmr_backend', run_once=True)
self.assertTrue(raven_mock.called)
self.assertTrue(format_tb_mock.called)

def test_supported_vendors(self):
""" Check whether supported vendors is as expected. """
Expand Down
10 changes: 9 additions & 1 deletion dsmr_consumption/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,17 @@ def day_consumption(day):
)
consumption['total_cost'] += consumption['gas_cost']

# Current prices as well.
consumption['energy_supplier_price_electricity_delivered_1'] = daily_energy_price.electricity_delivered_1_price
consumption['energy_supplier_price_electricity_delivered_2'] = daily_energy_price.electricity_delivered_2_price
consumption['energy_supplier_price_electricity_returned_1'] = daily_energy_price.electricity_returned_1_price
consumption['energy_supplier_price_electricity_returned_2'] = daily_energy_price.electricity_returned_2_price
consumption['energy_supplier_price_gas'] = daily_energy_price.gas_price

# Any notes of that day.
consumption['notes'] = Note.objects.filter(day=day).values_list('description', flat=True)

# Remperature readings are not mandatory as well.
# Temperature readings are not mandatory as well.
temperature_readings = TemperatureReading.objects.filter(
read_at__gte=day_start, read_at__lt=day_end,
).order_by('read_at')
Expand Down
12 changes: 12 additions & 0 deletions dsmr_consumption/tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,23 @@ def test_day_consumption(self):
self.assertEqual(data['electricity1_cost'], Decimal('0.25'))
self.assertEqual(data['electricity2_cost'], Decimal('0.75'))
self.assertEqual(data['total_cost'], 1)

self.assertEqual(data['energy_supplier_price_electricity_delivered_1'], 1)
self.assertEqual(data['energy_supplier_price_electricity_delivered_2'], 2)
self.assertEqual(data['energy_supplier_price_electricity_returned_1'], 0.5)
self.assertEqual(data['energy_supplier_price_electricity_returned_2'], 1.5)
self.assertEqual(data['energy_supplier_price_gas'], 5)
else:
self.assertEqual(data['electricity1_cost'], 0)
self.assertEqual(data['electricity2_cost'], 0)
self.assertEqual(data['total_cost'], 0)

self.assertEqual(data['energy_supplier_price_electricity_delivered_1'], 0)
self.assertEqual(data['energy_supplier_price_electricity_delivered_2'], 0)
self.assertEqual(data['energy_supplier_price_electricity_returned_1'], 0)
self.assertEqual(data['energy_supplier_price_electricity_returned_2'], 0)
self.assertEqual(data['energy_supplier_price_gas'], 0)

GasConsumption.objects.create(
read_at=now, # Now.
delivered=100,
Expand Down
109 changes: 109 additions & 0 deletions dsmr_datalogger/scripts/dsmr_datalogger_api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
NOTE: This script asumes:
- Smart meter supporting DSMR v4+ (see settings around line 40).
- The default serial port for the cable at ttyUSB0 (also see settings around line 40).
"""
from time import sleep

from serial.serialutil import SerialException
import requests
import serial


API_SERVERS = (
# You can add multiple hosts here... just copy the line below and paste it directly under it.
('http://HOST-OR-IP/api/v1/datalogger/dsmrreading', 'APIKEY-BLABLABLA-ABCDEFGHI'),
)


def main():
print('Starting...')

for telegram in read_telegram():
print('Telegram read')
print(telegram)

for current_server in API_SERVERS:
api_url, api_key = current_server

print('Sending telegram to:', api_url)

try:
send_telegram(telegram, api_url, api_key)
except Exception as error:
print('[!] {}'.format(error))

sleep(1)


def read_telegram():
""" Reads the serial port until we can create a reading point. """
serial_handle = serial.Serial()
serial_handle.port = '/dev/ttyUSB0'
serial_handle.baudrate = 115200
serial_handle.bytesize = serial.EIGHTBITS
serial_handle.parity = serial.PARITY_NONE
serial_handle.stopbits = serial.STOPBITS_ONE
serial_handle.xonxoff = 1
serial_handle.rtscts = 0
serial_handle.timeout = 20

# This might fail, but nothing we can do so just let it crash.
serial_handle.open()

telegram_start_seen = False
buffer = ''

# Just keep fetching data until we got what we were looking for.
while True:
try:
data = serial_handle.readline()
except SerialException as error:
# Something else and unexpected failed.
print('Serial connection failed:', error)
raise StopIteration() # Break out of yield.

try:
# Make sure weird characters are converted properly.
data = str(data, 'utf-8')
except TypeError:
pass

# This guarantees we will only parse complete telegrams. (issue #74)
if data.startswith('/'):
telegram_start_seen = True

# But make sure to RESET any data collected as well! (issue #212)
buffer = ''

# Delay any logging until we've seen the start of a telegram.
if telegram_start_seen:
buffer += data

# Telegrams ends with '!' AND we saw the start. We should have a complete telegram now.
if data.startswith('!') and telegram_start_seen:
yield buffer

# Reset the flow again.
telegram_start_seen = False
buffer = ''


def send_telegram(telegram, api_url, api_key):
# Register telegram by simply sending it to the application with a POST request.
response = requests.post(
api_url,
headers={'X-AUTHKEY': api_key},
data={'telegram': telegram},
)

# Old versions of DSMR-reader return 200, new ones 201.
if response.status_code not in (200, 201):
# Or you will find the error (hint) in the reponse body on failure.
print('API error: {}'.format(response.text))


if __name__ == '__main__':
main()
5 changes: 0 additions & 5 deletions dsmr_frontend/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,3 @@
class AppConfig(AppConfig):
name = 'dsmr_frontend'
verbose_name = _('Frontend')

def ready(self):
# For some weird reason Django proposes this model for DELETION when executing 'makemigrations'.
# This seems to prevent it somehow...
from .models.message import Notification # noqa: W0611
Loading

0 comments on commit 881b0e8

Please sign in to comment.