From 82f15621296bf9d6b816b1b210600fc24ad2fd01 Mon Sep 17 00:00:00 2001 From: Christian Ehrlicher Date: Wed, 17 Aug 2022 20:16:47 +0200 Subject: [PATCH 1/2] Initial plugin to directly send the values from the inverter to the volkszaehler middleware --- tools/rpi/ahoy.yml.example | 27 ++++++++++++ tools/rpi/hoymiles/__main__.py | 10 +++++ tools/rpi/hoymiles/outputs.py | 75 ++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index 15b2cd91b..a7afb01c0 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -28,6 +28,33 @@ ahoy: bucket: 'telegraf/autogen' measurement: 'hoymiles' + volkszaehler: + disabled: true + url: 'http://localhost/middleware/' + channels: + - type: 'temperature' + uid: 'ad578a40-1d97-11ed-8e8b-fda01a416575' + - type: 'frequency' + uid: '' + - type: 'ac_power0' + uid: '7ca5ac50-1e41-11ed-927f-610c4cb2c69e' + - type: 'ac_voltage0' + uid: '9a38e2e0-1d94-11ed-b539-25f8607ac030' + - type: 'ac_current0' + uid: 'a9a4daf0-1e41-11ed-b68c-eb73eef3d21d' + - type: 'dc_power0' + uid: '38eb3ca0-1e53-11ed-b830-792e70a592fa' + - type: 'dc_voltage0' + uid: '' + - type: 'dc_current0' + uid: '' + - type: 'dc_power1' + uid: '51c0e9d0-1e53-11ed-b574-8bc81547eb8f' + - type: 'dc_voltage1' + uid: '' + - type: 'dc_current1' + uid: '' + dtu: serial: 99978563001 diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index f41dd7da0..94fa8ff58 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -100,6 +100,9 @@ def poll_inverter(inverter, retries=4): if influx_client: influx_client.store_status(result) + if volkszaehler_client: + volkszaehler_client.store_status(result) + def mqtt_send_status(broker, inverter_ser, data, topic=None): """ Publish StatusResponse object @@ -247,6 +250,13 @@ def mqtt_on_command(client, userdata, message): bucket=influx_config.get('bucket', None), measurement=influx_config.get('measurement', 'hoymiles')) + volkszaehler_client = None + volkszaehler_config = ahoy_config.get('volkszaehler', {}) + if volkszaehler_config and not volkszaehler_config.get('disabled', False): + from .outputs import VolkszaehlerOutputPlugin + volkszaehler_client = VolkszaehlerOutputPlugin( + volkszaehler_config) + g_inverters = [g_inverter.get('serial') for g_inverter in ahoy_config.get('inverters', [])] for g_inverter in ahoy_config.get('inverters', []): g_inverter_ser = g_inverter.get('serial') diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index 62a4cb938..b557871ee 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -201,3 +201,78 @@ def store_status(self, response, **params): self.client.publish(f'{topic}/pf', data['powerfactor']) self.client.publish(f'{topic}/frequency', data['frequency']) self.client.publish(f'{topic}/temperature', data['temperature']) + +try: + import requests + import time +except ModuleNotFoundError: + pass + +class VolkszaehlerOutputPlugin(OutputPluginFactory): + def __init__(self, config, **params): + """ + Initialize VolkszaehlerOutputPlugin + """ + super().__init__(**params) + + self.baseurl = config.get('url', 'http://localhost/middleware/') + self.channels = dict() + for channel in config.get('channels', []): + uid = channel.get('uid') + ctype = channel.get('type') + if uid and ctype: + self.channels[ctype] = uid + + def store_status(self, response, **params): + """ + Publish StatusResponse object + + :param hoymiles.decoders.StatusResponse response: StatusResponse object + + :raises ValueError: when response is not instance of StatusResponse + """ + + if not isinstance(response, StatusResponse): + raise ValueError('Data needs to be instance of StatusResponse') + + if len(self.channels) == 0: + return + + data = response.__dict__() + + ts = int(round(time.time() * 1000)) + + # AC Data + phase_id = 0 + for phase in data['phases']: + self.try_publish(ts, f'ac_power{phase_id}', phase['power']) + self.try_publish(ts, f'ac_voltage{phase_id}', phase['voltage']) + self.try_publish(ts, f'ac_current{phase_id}', phase['current']) + phase_id = phase_id + 1 + + # DC Data + string_id = 0 + for string in data['strings']: + self.try_publish(ts, f'dc_power{string_id}', string['power']) + self.try_publish(ts, f'dc_voltage{string_id}', string['voltage']) + self.try_publish(ts, f'dc_current{string_id}', string['current']) + self.try_publish(ts, f'dc_total{string_id}', string['energy_total']) + self.try_publish(ts, f'dc_daily{string_id}', string['energy_daily']) + string_id = string_id + 1 + # Global + if data['powerfactor'] is not None: + self.try_publish(ts, f'powerfactor', data['powerfactor']) + self.try_publish(ts, f'frequency', data['frequency']) + self.try_publish(ts, f'temperature', data['temperature']) + + def try_publish(self, ts, ctype, value): + if not ctype in self.channels: + return + uid = self.channels[ctype] + url = f'{self.baseurl}/data/{uid}.json?operation=add&ts={ts}&value={value}' + try: + r = requests.get(url) + if r.status_code != 200: + raise ValueError('Could not send request (%s)' % url) + except ConnectionError as e: + raise ValueError('Could not send request (%s)' % e) From 6753be8871376eeb299e02f81b2761c49c8fa986 Mon Sep 17 00:00:00 2001 From: Christian Ehrlicher Date: Thu, 18 Aug 2022 18:06:20 +0200 Subject: [PATCH 2/2] Take timestamp from response instead local time --- tools/rpi/hoymiles/outputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index b557871ee..0bfaecac6 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -240,7 +240,7 @@ def store_status(self, response, **params): data = response.__dict__() - ts = int(round(time.time() * 1000)) + ts = int(round(data['time'].timestamp() * 1000)) # AC Data phase_id = 0