From 96d64faf627582376fa3e144de79789095f8ec02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= Date: Wed, 25 Jan 2023 11:49:13 +0100 Subject: [PATCH] RPi: send value irradiation to VZ and MQTT to send all meassured and calculated values to Volkszaehler and to mqtt change some logging levels for better differentiation change frequency as part of AC phase --- tools/rpi/ahoy.yml.example | 28 +++++- tools/rpi/hoymiles/__init__.py | 2 +- tools/rpi/hoymiles/__main__.py | 6 +- tools/rpi/hoymiles/decoders/__init__.py | 36 ++++--- tools/rpi/hoymiles/outputs.py | 126 +++++++++++++++--------- 5 files changed, 132 insertions(+), 66 deletions(-) diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index 7cb0c3b7..c157e826 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -44,9 +44,7 @@ ahoy: - serial: 114172220003 url: 'http://localhost/middleware/' channels: - - type: 'temperature' - uid: 'ad578a40-1d97-11ed-8e8b-fda01a416575' - - type: 'frequency' + - type: 'ac_frequency0' uid: '' - type: 'ac_power0' uid: '7ca5ac50-1e41-11ed-927f-610c4cb2c69e' @@ -54,18 +52,42 @@ ahoy: uid: '9a38e2e0-1d94-11ed-b539-25f8607ac030' - type: 'ac_current0' uid: 'a9a4daf0-1e41-11ed-b68c-eb73eef3d21d' + - type: 'ac_reactive_power0' + uid: '' - type: 'dc_power0' uid: '38eb3ca0-1e53-11ed-b830-792e70a592fa' - type: 'dc_voltage0' uid: '' - type: 'dc_current0' uid: '' + - type: 'dc_energy_total0' + uid: '' + - type: 'dc_energy_daily0' + uid: 'c2a93ea0-9a4e-11ed-8000-7d82e3ac8959' + - type: 'dc_irradiation0' + uid: 'c2d887a0-9a4e-11ed-a7ac-0dab944fd82d' - type: 'dc_power1' uid: '51c0e9d0-1e53-11ed-b574-8bc81547eb8f' - type: 'dc_voltage1' uid: '' - type: 'dc_current1' uid: '' + - type: 'dc_energy_total1' + uid: '' + - type: 'dc_energy_daily1' + uid: 'c3c04df0-9a4e-11ed-82c6-a15a9aba54a3' + - type: 'dc_irradiation1' + uid: 'c3f3efd0-9a4e-11ed-9a77-3fd3187e6237' + - type: 'temperature' + uid: 'ad578a40-1d97-11ed-8e8b-fda01a416575' + - type: 'powerfactor' + uid: '' + - type: 'yield_total' + uid: '' + - type: 'yield_today' + uid: 'c4a76dd0-9a4e-11ed-b79f-2de013d39150' + - type: 'efficiency' + uid: 'c4d8e9c0-9a4e-11ed-9d9e-9737749e4b45' dtu: serial: 99978563001 diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index 4b46a95e..74ec9fd8 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -159,7 +159,7 @@ class ResponseDecoder(ResponseDecoderFactory): command = self.request_command c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f") - logging.debug(f'{c_datetime} model_decoder: {model}Decode{command.upper()}') + logging.info(f'{c_datetime} model_decoder: {model}Decode{command.upper()}') model_decoders = __import__('hoymiles.decoders') if hasattr(model_decoders, f'{model}Decode{command.upper()}'): diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index c5451fb0..1dfc8321 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -17,7 +17,7 @@ from suntimes import SunTimes import argparse import yaml from yaml.loader import SafeLoader -import paho.mqtt.client +# import paho.mqtt.client import hoymiles import logging @@ -174,7 +174,7 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): # get decoder object result = decoder.decode() if hoymiles.HOYMILES_DEBUG_LOGGING: - logging.debug(f'{c_datetime} Decoded: {result.__dict__()}') + logging.info(f'{c_datetime} Decoded: {result.__dict__()}') # check decoder object for output if isinstance(result, hoymiles.decoders.StatusResponse): @@ -284,7 +284,7 @@ if __name__ == '__main__': logging.error("Could not load config file. Try --help") sys.exit(2) except yaml.YAMLError as e_yaml: - logging.error('Failed to load config file {global_config.config_file}: {e_yaml}') + logging.error(f'Failed to load config file {global_config.config_file}: {e_yaml}') sys.exit(1) # read AHOY configuration file and prepare logging diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index 040f67ef..b46dbe48 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -93,7 +93,8 @@ class Response: class StatusResponse(Response): """Inverter StatusResponse object""" - e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor', 'reactive_power', 'irradiation'] + phase_keys = ['voltage','current','power','reactive_power','frequency'] + string_keys = ['voltage','current','power','energy_total','energy_daily', 'irradiation'] temperature = None frequency = None powerfactor = None @@ -125,7 +126,7 @@ class StatusResponse(Response): p_exists = False phase_id = len(phases) phase = {} - for key in self.e_keys: + for key in self.phase_keys: prop = f'ac_{key}_{phase_id}' if hasattr(self, prop): p_exists = True @@ -149,7 +150,7 @@ class StatusResponse(Response): s_exists = False string_id = len(strings) string = {} - for key in self.e_keys: + for key in self.string_keys: prop = f'dc_{key}_{string_id}' if hasattr(self, prop): s_exists = True @@ -170,14 +171,27 @@ class StatusResponse(Response): data['phases'] = self.phases data['strings'] = self.strings data['temperature'] = self.temperature - data['frequency'] = self.frequency data['powerfactor'] = self.powerfactor - data['event_count'] = self.event_count - data['time'] = self.time_rx - data['energy_total'] = 0.0 + data['yield_total'] = 0.0 + data['yield_today'] = 0.0 + for string in data['strings']: + data['yield_total'] += string['energy_total'] + data['yield_today'] += string['energy_daily'] + + ac_sum_power = 0.0 + for phase in data['phases']: + ac_sum_power += phase['power'] + dc_sum_power = 0.0 for string in data['strings']: - data['energy_total'] += string['energy_total'] + dc_sum_power += string['power'] + if dc_sum_power != 0: + data['efficiency'] = round(ac_sum_power * 100 / dc_sum_power, 2) + else: + data['efficiency'] = 0.0 + + data['event_count'] = self.event_count + data['time'] = self.time_rx return data @@ -469,7 +483,7 @@ class Hm300Decode0B(StatusResponse): """ Phase 1 watts """ return self.unpack('>H', 18)[0]/10 @property - def frequency(self): + def ac_frequency_0(self): """ Grid frequency in Hertz """ return self.unpack('>H', 16)[0]/100 @property @@ -568,7 +582,7 @@ class Hm600Decode0B(StatusResponse): """ Phase 1 watts """ return self.unpack('>H', 30)[0]/10 @property - def frequency(self): + def ac_frequency_0(self): """ Grid frequency in Hertz """ return self.unpack('>H', 28)[0]/100 @property @@ -729,7 +743,7 @@ class Hm1200Decode0B(StatusResponse): """ Phase 1 watts """ return self.unpack('>H', 50)[0]/10 @property - def frequency(self): + def ac_frequency_0(self): """ Grid frequency in Hertz """ return self.unpack('>H', 48)[0]/100 @property diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index 64763c37..18af494a 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -10,11 +10,6 @@ import logging from datetime import datetime, timezone from hoymiles.decoders import StatusResponse, HardwareInfoResponse -try: - from influxdb_client import InfluxDBClient -except ModuleNotFoundError: - pass - class OutputPluginFactory: def __init__(self, **params): """ @@ -59,10 +54,19 @@ class InfluxOutputPlugin(OutputPluginFactory): """ super().__init__(**params) + try: + from influxdb_client import InfluxDBClient + except ModuleNotFoundError: + ErrorText1 = f'Module "influxdb_client" for INFLUXDB necessary.' + ErrorText2 = f'Install module with command: python3 -m pip install influxdb_client' + print(ErrorText1, ErrorText2) + logging.error(ErrorText1) + logging.error(ErrorText2) + exit() + self._bucket = params.get('bucket', 'hoymiles/autogen') self._org = params.get('org', '') - self._measurement = params.get('measurement', - f'inverter,host={socket.gethostname()}') + self._measurement = params.get('measurement', f'inverter,host={socket.gethostname()}') client = InfluxDBClient(url, token, bucket=self._bucket) self.api = client.write_api() @@ -105,6 +109,7 @@ class InfluxOutputPlugin(OutputPluginFactory): data_stack.append(f'{measurement},phase={phase_id},type=current value={phase["current"]} {ctime}') data_stack.append(f'{measurement},phase={phase_id},type=power value={phase["power"]} {ctime}') data_stack.append(f'{measurement},phase={phase_id},type=Q_AC value={phase["reactive_power"]} {ctime}') + data_stack.append(f'{measurement},phase={phase_id},type=frequency value={phase["frequency"]:.3f} {ctime}') phase_id = phase_id + 1 # DC Data @@ -115,28 +120,23 @@ class InfluxOutputPlugin(OutputPluginFactory): data_stack.append(f'{measurement},string={string_id},type=power value={string["power"]:.2f} {ctime}') data_stack.append(f'{measurement},string={string_id},type=YieldDay value={string["energy_daily"]:.2f} {ctime}') data_stack.append(f'{measurement},string={string_id},type=YieldTotal value={string["energy_total"]/1000:.4f} {ctime}') - if 'irradiation' in string: - data_stack.append(f'{measurement},string={string_id},type=Irradiation value={string["irradiation"]:.2f} {ctime}') + data_stack.append(f'{measurement},string={string_id},type=Irradiation value={string["irradiation"]:.2f} {ctime}') string_id = string_id + 1 # Global if data['event_count'] is not None: data_stack.append(f'{measurement},type=total_events value={data["event_count"]} {ctime}') if data['powerfactor'] is not None: - data_stack.append(f'{measurement},type=pf value={data["powerfactor"]:f} {ctime}') - data_stack.append(f'{measurement},type=frequency value={data["frequency"]:.3f} {ctime}') - + data_stack.append(f'{measurement},type=PF_AC value={data["powerfactor"]:f} {ctime}') data_stack.append(f'{measurement},type=Temp value={data["temperature"]:.2f} {ctime}') - if data['energy_total'] is not None: - data_stack.append(f'{measurement},type=total value={data["energy_total"]/1000:.3f} {ctime}') + if data['yield_total'] is not None: + data_stack.append(f'{measurement},type=YieldTotal value={data["yield_total"]/1000:.3f} {ctime}') + if data['yield_today'] is not None: + data_stack.append(f'{measurement},type=YieldToday value={data["yield_today"]/1000:.3f} {ctime}') + data_stack.append(f'{measurement},type=Efficiency value={data["efficiency"]:.2f} {ctime}') self.api.write(self._bucket, self._org, data_stack) -try: - import paho.mqtt.client -except ModuleNotFoundError: - pass - class MqttOutputPlugin(OutputPluginFactory): """ Mqtt output plugin """ client = None @@ -164,6 +164,16 @@ class MqttOutputPlugin(OutputPluginFactory): """ super().__init__(**params) + try: + import paho.mqtt.client + except ModuleNotFoundError: + ErrorText1 = f'Module "paho.mqtt.client" for MQTT-output necessary.' + ErrorText2 = f'Install module with command: python3 -m pip install paho-mqtt' + print(ErrorText1, ErrorText2) + logging.error(ErrorText1) + logging.error(ErrorText2) + exit() + mqtt_client = paho.mqtt.client.Client() if config.get('useTLS',False): mqtt_client.tls_set() @@ -196,33 +206,41 @@ class MqttOutputPlugin(OutputPluginFactory): # AC Data phase_id = 0 + phase_sum_power = 0 for phase in data['phases']: - self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) + self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) self.client.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power']) + self.client.publish(f'{topic}/emeter/{phase_id}/frequency', phase['frequency']) phase_id = phase_id + 1 + phase_sum_power += phase['power'] # DC Data string_id = 0 + string_sum_power = 0 for string in data['strings']: self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily']) self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000) - if 'irradiation' in string: - self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation']) + self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation']) string_id = string_id + 1 + string_sum_power += string['power'] # Global + if data['event_count'] is not None: + self.client.publish(f'{topic}/total_events', data['event_count']) if data['powerfactor'] is not None: - self.client.publish(f'{topic}/pf', data['powerfactor']) - self.client.publish(f'{topic}/frequency', data['frequency']) - + self.client.publish(f'{topic}/PF_AC', data['powerfactor']) self.client.publish(f'{topic}/Temp', data['temperature']) - if data['energy_total'] is not None: - self.client.publish(f'{topic}/total', data['energy_total']/1000) + if data['yield_total'] is not None: + self.client.publish(f'{topic}/YieldTotal', data['yield_total']/1000) + if data['yield_today'] is not None: + self.client.publish(f'{topic}/YieldToday', data['yield_today']/1000) + self.client.publish(f'{topic}/Efficiency', data['efficiency']) + elif isinstance(response, HardwareInfoResponse): self.client.publish(f'{topic}/Firmware/Version',\ @@ -236,12 +254,6 @@ class MqttOutputPlugin(OutputPluginFactory): else: raise ValueError('Data needs to be instance of StatusResponse or a instance of HardwareInfoResponse') -try: - import requests - import time -except ModuleNotFoundError: - pass - class VzInverterOutput: def __init__(self, config, session): self.session = session @@ -274,7 +286,8 @@ class VzInverterOutput: self.try_publish(ts, f'ac_voltage{phase_id}', phase['voltage']) self.try_publish(ts, f'ac_current{phase_id}', phase['current']) self.try_publish(ts, f'ac_power{phase_id}', phase['power']) - self.try_publish(ts, f'ac_Q{phase_id}', phase['reactive_power']) + self.try_publish(ts, f'ac_reactive_power{phase_id}', phase['reactive_power']) + self.try_publish(ts, f'ac_frequency{phase_id}', phase['frequency']) phase_id = phase_id + 1 # DC Data @@ -283,33 +296,39 @@ class VzInverterOutput: 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_power{string_id}', string['power']) - self.try_publish(ts, f'dc_YieldDay{string_id}', string['energy_daily']) - self.try_publish(ts, f'dc_YieldTotal{string_id}', string['energy_total']) - if 'irradiation' in string: - self.try_publish(ts, f'dc_Irradiation{string_id}', string['irradiation']) + self.try_publish(ts, f'dc_energy_daily{string_id}', string['energy_daily']) + self.try_publish(ts, f'dc_energy_total{string_id}', string['energy_total']) + self.try_publish(ts, f'dc_irradiation{string_id}', string['irradiation']) string_id = string_id + 1 # Global + if data['event_count'] is not None: + self.try_publish(ts, f'event_count', data['event_count']) 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'Temp', data['temperature']) - if data['energy_total'] is not None: - self.try_publish(ts, f'total', data['energy_total']) - + self.try_publish(ts, f'temperature', data['temperature']) + if data['yield_total'] is not None: + self.try_publish(ts, f'yield_total', data['yield_total']) + if data['yield_today'] is not None: + self.try_publish(ts, f'yield_today', data['yield_today']) + self.try_publish(ts, f'efficiency', data['efficiency']) def try_publish(self, ts, ctype, value): if not ctype in self.channels: + logging.warning(f'ctype \"{ctype}\" not found in ahoy.yml') return uid = self.channels[ctype] url = f'{self.baseurl}/data/{uid}.json?operation=add&ts={ts}&value={value}' try: r = self.session.get(url) - if r.status_code != 200: - raise ValueError('Could not send request (%s)' % url) - except requests.exceptions.ConnectionError as e: - raise ValueError('Could not send request (%s)' % e) + if r.status_code == 404: + logging.critical('VZ-DB not reachable, please check "middleware"') + if r.status_code == 400: + logging.critical('UUID not configured in VZ-DB') + elif r.status_code != 200: + raise ValueError(f'Transmit result {url}') + except ConnectionError as e: + raise ValueError(f'Could not connect VZ-DB {type(e)} {e.keys()}') class VolkszaehlerOutputPlugin(OutputPluginFactory): def __init__(self, config, **params): @@ -318,6 +337,17 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory): """ super().__init__(**params) + try: + import requests + import time + except ModuleNotFoundError: + ErrorText1 = f'Module "requests" and "time" for VolkszaehlerOutputPlugin necessary.' + ErrorText2 = f'Install module with command: python3 -m pip install requests' + print(ErrorText1, ErrorText2) + logging.error(ErrorText1) + logging.error(ErrorText2) + exit(1) + self.session = requests.Session() self.inverters = dict()