From e5b427f4f233f6c5d15f70d5a62934dd1b1e8374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Jonas=20S=C3=A4mann?= Date: Sun, 15 May 2022 16:04:38 +0200 Subject: [PATCH 01/10] Provice decoders super class to ensure properties --- tools/rpi/hoymiles/decoders/__init__.py | 97 +++++++++++++++++-------- 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index 68bef7f5..de1d8b25 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -2,13 +2,43 @@ # -*- coding: utf-8 -*- import struct import crcmod -from datetime import timedelta +from datetime import datetime, timedelta f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') -class StatusResponse: +class Response: + inverter_ser = None + inverter_name = None + dtu_ser = None + response = None + + def __init__(self, *args, **params): + """ + :param bytes response: response payload bytes + """ + self.inverter_ser = params.get('inverter_ser', None) + self.inverter_name = params.get('inverter_name', None) + self.dtu_ser = params.get('dtu_ser', None) + + self.response = args[0] + + if isinstance(params.get('time_rx', None), datetime): + self.time_rx = params['time_rx'] + else: + self.time_rx = datetime.now() + + def __dict__(self): + return { + 'inverter_ser': self.inverter_ser, + 'inverter_name': self.inverter_name, + 'dtu_ser': self.dtu_ser} + +class StatusResponse(Response): """Inverter StatusResponse object""" - e_keys = ['voltage','current','power','energy_total','energy_daily'] + e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor'] + + def __init__(self, *args, **params): + super().__init__(*args, **params) def unpack(self, fmt, base): """ @@ -77,17 +107,22 @@ class StatusResponse: :return: dict of properties :rtype: dict """ - data = {} + data = super().__dict__() data['phases'] = self.phases data['strings'] = self.strings data['temperature'] = self.temperature data['frequency'] = self.frequency + data['time'] = self.time_rx return data -class UnknownResponse: +class UnknownResponse(Response): """ Debugging helper for unknown payload format """ + + def __init__(self, *args, **params): + super().__init__(*args, **params) + @property def hex_ascii(self): """ @@ -291,8 +326,8 @@ class EventsResponse(UnknownResponse): 9000: 'Microinverter is suspected of being stolen' } - def __init__(self, response): - self.response = response + def __init__(self, *args, **params): + super().__init__(*args, **params) crc_valid = self.valid_crc if crc_valid: @@ -318,13 +353,9 @@ class EventsResponse(UnknownResponse): class DEBUG_DecodeAny(UnknownResponse): """Default decoder""" - def __init__(self, response): - """ - Try interpret and print unknown response data - :param bytes response: response payload bytes - """ - self.response = response + def __init__(self, *args, **params): + super().__init__(*args, **params) crc_valid = self.valid_crc if crc_valid: @@ -385,8 +416,8 @@ class DEBUG_DecodeAny(UnknownResponse): # 1121-Series Intervers, 1 MPPT, 1 Phase class HM300_Decode0B(StatusResponse): - def __init__(self, response): - self.response = response + def __init__(self, *args, **params): + super().__init__(*args, **params) @property def dc_voltage_0(self): @@ -422,19 +453,18 @@ class HM300_Decode0B(StatusResponse): return self.unpack('>H', 26)[0]/10 class HM300_Decode11(EventsResponse): - def __init__(self, response): - super().__init__(response) + def __init__(self, *args, **params): + super().__init__(*args, **params) class HM300_Decode12(EventsResponse): - def __init__(self, response): - super().__init__(response) - + def __init__(self, *args, **params): + super().__init__(*args, **params) # 1141-Series Inverters, 2 MPPT, 1 Phase class HM600_Decode0B(StatusResponse): - def __init__(self, response): - self.response = response + def __init__(self, *args, **params): + super().__init__(*args, **params) @property def dc_voltage_0(self): @@ -483,20 +513,23 @@ class HM600_Decode0B(StatusResponse): @property def temperature(self): return self.unpack('>H', 38)[0]/10 + @property + def alarm_count(self): + return self.unpack('>H', 40)[0] class HM600_Decode11(EventsResponse): - def __init__(self, response): - super().__init__(response) + def __init__(self, *args, **params): + super().__init__(*args, **params) class HM600_Decode12(EventsResponse): - def __init__(self, response): - super().__init__(response) + def __init__(self, *args, **params): + super().__init__(*args, **params) # 1161-Series Inverters, 4 MPPT, 1 Phase class HM1200_Decode0B(StatusResponse): - def __init__(self, response): - self.response = response + def __init__(self, *args, **params): + super().__init__(*args, **params) @property def dc_voltage_0(self): @@ -579,9 +612,9 @@ class HM1200_Decode0B(StatusResponse): return self.unpack('>H', 58)[0]/10 class HM1200_Decode11(EventsResponse): - def __init__(self, response): - super().__init__(response) + def __init__(self, *args, **params): + super().__init__(*args, **params) class HM1200_Decode12(EventsResponse): - def __init__(self, response): - super().__init__(response) + def __init__(self, *args, **params): + super().__init__(*args, **params) From 7415139038b6c1b851d55caec34999493ff23211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Jonas=20S=C3=A4mann?= Date: Sun, 15 May 2022 16:06:38 +0200 Subject: [PATCH 02/10] Provide rx time in response --- tools/rpi/hoymiles/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index 710595e9..af2d2f76 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -67,14 +67,19 @@ class ResponseDecoderFactory: :type request: bytes :param inverter_ser: inverter serial :type inverter_ser: str + :param time_rx: idatetime when payload was received + :type time_rx: datetime """ model = None request = None response = None + time_rx = None def __init__(self, response, **params): self.response = response + self.time_rx = params.get('time_rx', datetime.now()) + if 'request' in params: self.request = params['request'] elif hasattr(response, 'request'): @@ -164,7 +169,10 @@ class ResponseDecoder(ResponseDecoderFactory): if HOYMILES_DEBUG_LOGGING: device = getattr(model_decoders, f'DEBUG_DecodeAny') - return device(self.response) + return device(self.response, + time_rx=self.time_rx, + inverter_ser=self.inverter_ser + ) class InverterPacketFragment: """ESB Frame""" From 5101b22ff7f5429712ec66d93e7bce12e7ceb40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Jonas=20S=C3=A4mann?= Date: Sun, 15 May 2022 16:09:37 +0200 Subject: [PATCH 03/10] Provide Influx2 output plugin Allready thougt about it earlier and requested in #29 --- tools/rpi/ahoy.yml.example | 9 ++ tools/rpi/hoymiles/__main__.py | 13 +++ tools/rpi/hoymiles/outputs.py | 138 ++++++++++++++++++++++++++++ tools/rpi/optional-requirements.txt | 1 + 4 files changed, 161 insertions(+) create mode 100644 tools/rpi/hoymiles/outputs.py create mode 100644 tools/rpi/optional-requirements.txt diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index 5c27c003..ef139c48 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -16,6 +16,15 @@ ahoy: user: 'username' password: 'password' + # Influx2 output + influxdb: + disabled: true + url: 'http://influxserver.local:8086' + org: 'myorg' + token: '' + bucket: 'telegraf/autogen' + measurement: 'hoymiles' + dtu: serial: 99978563001 diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 0a8c435e..d97e1091 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -91,6 +91,8 @@ def poll_inverter(inverter, retries=4): if mqtt_client: mqtt_send_status(mqtt_client, inverter_ser, data, topic=inverter.get('mqtt', {}).get('topic', None)) + if influx_client: + influx_client.store_status(result) def mqtt_send_status(broker, inverter_ser, data, topic=None): """ @@ -225,6 +227,17 @@ if __name__ == '__main__': mqtt_client.loop_start() mqtt_client.on_message = mqtt_on_command + influx_client = None + influx_config = ahoy_config.get('influxdb', {}) + if influx_config and not influx_config.get('disabled', False): + from .outputs import InfluxOutputPlugin + influx_client = InfluxOutputPlugin( + influx_config.get('url'), + influx_config.get('token'), + org=influx_config.get('org', ''), + bucket=influx_config.get('bucket', None), + measurement=influx_config.get('measurement', 'hoymiles')) + if not radio.begin(): raise RuntimeError('Can\'t open radio') diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py new file mode 100644 index 00000000..f1b6a2d1 --- /dev/null +++ b/tools/rpi/hoymiles/outputs.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 + +import socket +from datetime import datetime, timezone +from hoymiles.decoders import StatusResponse + +try: + from influxdb_client import InfluxDBClient +except ModuleNotFoundError: + pass + +class OutputPluginFactory: + def __init__(self, **params): + """Initialize output plugin""" + + self.inverter_ser = params.get('inverter_ser', 0) + + def store_status(self, data): + raise NotImplementedError('The current output plugin does not implement store_status') + +class InfluxOutputPlugin(OutputPluginFactory): + def __init__(self, url, token, **params): + super().__init__(**params) + + self._bucket = params.get('bucket', 'hoymiles/autogen') + self._org = params.get('org', '') + self._measurement = params.get('measurement', + f'inverter,host={socket.gethostname()}') + + client = InfluxDBClient(url, token, bucket=self._bucket) + self.api = client.write_api() + + def store_status(self, response): + """ + Publish StatusResponse object + + :param influxdb.InfluxDBClient influx_client: A connected instance to Influx database + :param str inverter_ser: inverter serial + :param hoymiles.StatusResponse data: decoded inverter StatusResponse + :type response: hoymiles.StatusResponse + :param measurement: Influx measurement name + :type measurement: str + """ + + if not isinstance(response, StatusResponse): + raise RuntimeError('Data needs to be instance of StatusResponse') + + data = response.__dict__() + + measurement = self._measurement + f',location={data["inverter_ser"]}' + + data_stack = [] + + time_rx = datetime.now() + if 'time' in data and isinstance(data['time'], datetime): + time_rx = data['time'] + + # InfluxDB uses UTC + utctime = datetime.fromtimestamp(time_rx.timestamp(), tz=timezone.utc) + + # InfluxDB requires nanoseconds + ctime = int(utctime.timestamp() * 1e9) + + # AC Data + phase_id = 0 + for phase in data['phases']: + data_stack.append(f'{measurement},phase={phase_id},type=power value={phase["power"]} {ctime}') + data_stack.append(f'{measurement},phase={phase_id},type=voltage value={phase["voltage"]} {ctime}') + data_stack.append(f'{measurement},phase={phase_id},type=current value={phase["current"]} {ctime}') + phase_id = phase_id + 1 + + # DC Data + string_id = 0 + for string in data['strings']: + data_stack.append(f'{measurement},string={string_id},type=total value={string["energy_total"]/1000:.4f} {ctime}') + data_stack.append(f'{measurement},string={string_id},type=power value={string["power"]:.2f} {ctime}') + data_stack.append(f'{measurement},string={string_id},type=voltage value={string["voltage"]:.3f} {ctime}') + data_stack.append(f'{measurement},string={string_id},type=current value={string["current"]:3f} {ctime}') + string_id = string_id + 1 + # Global + data_stack.append(f'{measurement},type=frequency value={data["frequency"]:.3f} {ctime}') + data_stack.append(f'{measurement},type=temperature value={data["temperature"]:.2f} {ctime}') + + self.api.write(self._bucket, self._org, data_stack) + +try: + import paho.mqtt.client +except ModuleNotFoundError: + pass + +class MqttOutputPlugin(OutputPluginFactory): + def __init__(self, *args, **params): + super().__init__(*args, **params) + + mqtt_client = paho.mqtt.client.Client() + mqtt_client.username_pw_set(params.get('user', None), params.get('password', None)) + mqtt_client.connect(params.get('host', '127.0.0.1'), params.get('port', 1883)) + mqtt_client.loop_start() + + self.client = mqtt_client + + def store_status(self, response, **params): + """ + Publish StatusResponse object + + :param paho.mqtt.client.Client broker: mqtt-client instance + :param str inverter_ser: inverter serial + :param hoymiles.StatusResponse data: decoded inverter StatusResponse + :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) + :type topic: str + """ + + if not isinstance(response, StatusResponse): + raise RuntimeError('Data needs to be instance of StatusResponse') + + data = response.__dict__() + + topic = params.get('topic', f'hoymiles/{inverter_ser}') + + # AC Data + phase_id = 0 + for phase in data['phases']: + self.mqtt_client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) + self.mqtt_client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) + self.mqtt_client.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) + phase_id = phase_id + 1 + + # DC Data + string_id = 0 + for string in data['strings']: + self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000) + self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) + self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) + self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) + string_id = string_id + 1 + # Global + self.mqtt_client.publish(f'{topic}/frequency', data['frequency']) + self.mqtt_client.publish(f'{topic}/temperature', data['temperature']) diff --git a/tools/rpi/optional-requirements.txt b/tools/rpi/optional-requirements.txt new file mode 100644 index 00000000..19297f02 --- /dev/null +++ b/tools/rpi/optional-requirements.txt @@ -0,0 +1 @@ +influxdb-client>=1.28.0 From 2934c23688aaa65746f49faa4147bfaf7b51feaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Jonas=20S=C3=A4mann?= Date: Sun, 15 May 2022 18:26:59 +0200 Subject: [PATCH 04/10] Update UnknownResponse decoder --- tools/rpi/hoymiles/decoders/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index de1d8b25..cd0e87a0 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -148,7 +148,7 @@ class UnknownResponse(Response): @property def dump_longs(self): """Get all data, interpreted as long""" - if len(self.response) < 5: + if len(self.response) < 3: return None res = self.response @@ -166,7 +166,7 @@ class UnknownResponse(Response): @property def dump_longs_pad1(self): """Get all data, interpreted as long""" - if len(self.response) < 7: + if len(self.response) < 5: return None res = self.response[2:] @@ -184,7 +184,7 @@ class UnknownResponse(Response): @property def dump_longs_pad2(self): """Get all data, interpreted as long""" - if len(self.response) < 9: + if len(self.response) < 7: return None res = self.response[4:] @@ -202,7 +202,7 @@ class UnknownResponse(Response): @property def dump_longs_pad3(self): """Get all data, interpreted as long""" - if len(self.response) < 11: + if len(self.response) < 9: return None res = self.response[6:] @@ -220,7 +220,7 @@ class UnknownResponse(Response): @property def dump_shorts(self): """Get all data, interpreted as short""" - if len(self.response) < 5: + if len(self.response) < 3: return None res = self.response @@ -238,7 +238,7 @@ class UnknownResponse(Response): @property def dump_shorts_pad1(self): """Get all data, interpreted as short""" - if len(self.response) < 6: + if len(self.response) < 4: return None res = self.response[1:] @@ -332,7 +332,7 @@ class EventsResponse(UnknownResponse): crc_valid = self.valid_crc if crc_valid: print(' payload has valid modbus crc') - self.response = response[:-2] + self.response = self.response[:-2] status = self.response[:2] @@ -360,7 +360,7 @@ class DEBUG_DecodeAny(UnknownResponse): crc_valid = self.valid_crc if crc_valid: print(' payload has valid modbus crc') - self.response = response[:-2] + self.response = self.response[:-2] l_payload = len(self.response) print(f' payload has {l_payload} bytes') From e4eebb67e402aeb75e1f89ce2edf5de74c08814e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Jonas=20S=C3=A4mann?= Date: Mon, 16 May 2022 12:29:58 +0200 Subject: [PATCH 05/10] Improve python code quality Mostly fix naming convetion, add descriptions. Cleanup code, left behind from tinkering. --- tools/rpi/hoymiles/__init__.py | 112 ++++++++------- tools/rpi/hoymiles/__main__.py | 48 ++++--- tools/rpi/hoymiles/decoders/__init__.py | 173 +++++++++++++++--------- tools/rpi/hoymiles/outputs.py | 109 +++++++++++---- 4 files changed, 279 insertions(+), 163 deletions(-) diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index af2d2f76..63fe2ebf 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -1,62 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Hoymiles micro-inverters python shared code +""" + import struct -import crcmod -import json import time import re from datetime import datetime +import json +import crcmod from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16 from .decoders import * f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0) - HOYMILES_TRANSACTION_LOGGING=False HOYMILES_DEBUG_LOGGING=False -def ser_to_hm_addr(s): +def ser_to_hm_addr(inverter_ser): """ - Calculate the 4 bytes that the HM devices use in their internal messages to + Calculate the 4 bytes that the HM devices use in their internal messages to address each other. - :param str s: inverter serial + :param str inverter_ser: inverter serial :return: inverter address :rtype: bytes """ - bcd = int(str(s)[-8:], base=16) + bcd = int(str(inverter_ser)[-8:], base=16) return struct.pack('>L', bcd) -def ser_to_esb_addr(s): +def ser_to_esb_addr(inverter_ser): """ Convert a Hoymiles inverter/DTU serial number into its corresponding NRF24 'enhanced shockburst' address byte sequence (5 bytes). The NRF library expects these in LSB to MSB order, even though the transceiver itself will then output them in MSB-to-LSB order over the air. - + The inverters use a BCD representation of the last 8 - digits of their serial number, in reverse byte order, + digits of their serial number, in reverse byte order, followed by \x01. - :param str s: inverter serial + :param str inverter_ser: inverter serial :return: ESB inverter address :rtype: bytes """ - air_order = ser_to_hm_addr(s)[::-1] + b'\x01' + air_order = ser_to_hm_addr(inverter_ser)[::-1] + b'\x01' return air_order[::-1] -def print_addr(a): +def print_addr(inverter_ser): """ Debug print addresses - :param str a: inverter serial + :param str inverter_ser: inverter serial """ - print(f"ser# {a} ", end='') - print(f" -> HM {' '.join([f'{x:02x}' for x in ser_to_hm_addr(a)])}", end='') - print(f" -> ESB {' '.join([f'{x:02x}' for x in ser_to_esb_addr(a)])}") - -# time of last transmission - to calculcate response time -t_last_tx = 0 + print(f"ser# {inverter_ser} ", end='') + print(f" -> HM {' '.join([f'{byte:02x}' for byte in ser_to_hm_addr(inverter_ser)])}", end='') + print(f" -> ESB {' '.join([f'{byte:02x}' for byte in ser_to_esb_addr(inverter_ser)])}") class ResponseDecoderFactory: """ @@ -115,16 +118,16 @@ class ResponseDecoderFactory: raise ValueError('Inverter serial while decoding response') ser_db = [ - ('HM300', r'^1121........'), - ('HM600', r'^1141........'), - ('HM1200', r'^1161........'), + ('Hm300', r'^1121........'), + ('Hm600', r'^1141........'), + ('Hm1200', r'^1161........'), ] ser_str = str(self.inverter_ser) model = None - for m, r in ser_db: - if re.match(r, ser_str): - model = m + for s_model, r_match in ser_db: + if re.match(r_match, ser_str): + model = s_model break if len(model): @@ -162,12 +165,12 @@ class ResponseDecoder(ResponseDecoderFactory): model = self.inverter_model command = self.request_command - model_decoders = __import__(f'hoymiles.decoders') - if hasattr(model_decoders, f'{model}_Decode{command.upper()}'): - device = getattr(model_decoders, f'{model}_Decode{command.upper()}') + model_decoders = __import__('hoymiles.decoders') + if hasattr(model_decoders, f'{model}Decode{command.upper()}'): + device = getattr(model_decoders, f'{model}Decode{command.upper()}') else: if HOYMILES_DEBUG_LOGGING: - device = getattr(model_decoders, f'DEBUG_DecodeAny') + device = getattr(model_decoders, 'DebugDecodeAny') return device(self.response, time_rx=self.time_rx, @@ -188,6 +191,8 @@ class InverterPacketFragment: :type ch_rx: int :param ch_tx: channel where request was sent :type ch_tx: int + + :raises BufferError: when data gets lost on SPI bus """ if not time_rx: @@ -255,11 +260,11 @@ class InverterPacketFragment: :return: log line received frame :rtype: str """ - dt = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f") + c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f") size = len(self.frame) channel = f' channel {self.ch_rx}' if self.ch_rx else '' raw = " ".join([f"{b:02x}" for b in self.frame]) - return f"{dt} Received {size} bytes{channel}: {raw}" + return f"{c_datetime} Received {size} bytes{channel}: {raw}" class HoymilesNRF: """Hoymiles NRF24 Interface""" @@ -330,6 +335,7 @@ class HoymilesNRF: has_payload, pipe_number = self.radio.available_pipe() if has_payload: + # Data in nRF24 buffer, read it self.rx_error = 0 self.rx_channel_ack = True @@ -342,9 +348,11 @@ class HoymilesNRF: ch_rx=self.rx_channel, ch_tx=self.tx_channel, time_rx=datetime.now() ) - yield(fragment) + + yield fragment else: + # No data in nRF rx buffer, search and wait # Channel lock in (not currently used) self.rx_error = self.rx_error + 1 @@ -407,7 +415,7 @@ def frame_payload(payload): return payload -def compose_esb_fragment(fragment, seq=b'\80', src=99999999, dst=1, **params): +def compose_esb_fragment(fragment, seq=b'\x80', src=99999999, dst=1, **params): """ Build standart ESB request fragment @@ -423,20 +431,19 @@ def compose_esb_fragment(fragment, seq=b'\80', src=99999999, dst=1, **params): :raises ValueError: if fragment size larger 16 byte """ if len(fragment) > 17: - raise ValueError(f'ESB fragment exeeds mtu ({mtu}): Fragment size {len(fragment)} bytes') + raise ValueError(f'ESB fragment exeeds mtu: Fragment size {len(fragment)} bytes') - p = b'' - p = p + b'\x15' - p = p + ser_to_hm_addr(dst) - p = p + ser_to_hm_addr(src) - p = p + seq + packet = b'\x15' + packet = packet + ser_to_hm_addr(dst) + packet = packet + ser_to_hm_addr(src) + packet = packet + seq - p = p + fragment + packet = packet + fragment - crc8 = f_crc8(p) - p = p + struct.pack('B', crc8) + crc8 = f_crc8(packet) + packet = packet + struct.pack('B', crc8) - return p + return packet def compose_esb_packet(packet, mtu=17, **params): """ @@ -449,7 +456,7 @@ def compose_esb_packet(packet, mtu=17, **params): """ for i in range(0, len(packet), mtu): fragment = compose_esb_fragment(packet[i:i+mtu], **params) - yield(fragment) + yield fragment def compose_set_time_payload(timestamp=None): """ @@ -480,6 +487,7 @@ class InverterTransaction: inverter_addr = None dtu_ser = None req_type = None + time_rx = None radio = None @@ -538,15 +546,15 @@ class InverterTransaction: if not self.radio: return False - if not len(self.tx_queue): + if len(self.tx_queue) == 0: return False packet = self.tx_queue.pop(0) if HOYMILES_TRANSACTION_LOGGING: - dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") - print(f'{dt} Transmit {len(packet)} | {hexify_payload(packet)}') - + c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") + print(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}') + self.radio.transmit(packet) wait = False @@ -554,7 +562,7 @@ class InverterTransaction: for response in self.radio.receive(): if HOYMILES_TRANSACTION_LOGGING: print(response) - + self.frame_append(response) wait = True except TimeoutError: @@ -654,9 +662,9 @@ class InverterTransaction: :return: log line of payload for transmission :rtype: str """ - dt = self.request_time.strftime("%Y-%m-%d %H:%M:%S.%f") + c_datetime = self.request_time.strftime("%Y-%m-%d %H:%M:%S.%f") size = len(self.request) - return f'{dt} Transmit | {hexify_payload(self.request)}' + return f'{c_datetime} Transmit | {hexify_payload(self.request)}' def hexify_payload(byte_var): """ diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index d97e1091..f74828da 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -1,17 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" +Hoymiles micro-inverters main application +""" + import sys import struct import re import time from datetime import datetime import argparse -import hoymiles -from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16 -import paho.mqtt.client import yaml from yaml.loader import SafeLoader +import paho.mqtt.client +from RF24 import RF24, RF24_PA_LOW, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16 +import hoymiles def main_loop(): """Main loop""" @@ -61,14 +65,14 @@ def poll_inverter(inverter, retries=4): try: response = com.get_payload() payload_ttl = 0 - except Exception as e: - print(f'Error while retrieving data: {e}') + except Exception as e_all: + print(f'Error while retrieving data: {e_all}') pass # Handle the response data if any if response: - dt = datetime.now() - print(f'{dt} Payload: ' + hoymiles.hexify_payload(response)) + c_datetime = datetime.now() + print(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response)) decoder = hoymiles.ResponseDecoder(response, request=com.request, inverter_ser=inverter_ser @@ -77,7 +81,7 @@ def poll_inverter(inverter, retries=4): if isinstance(result, hoymiles.decoders.StatusResponse): data = result.__dict__() if hoymiles.HOYMILES_DEBUG_LOGGING: - print(f'{dt} Decoded: {data["temperature"]}', end='') + print(f'{c_datetime} Decoded: {data["temperature"]}', end='') phase_id = 0 for phase in data['phases']: print(f' phase{phase_id}=voltage:{phase["voltage"]}, current:{phase["current"]}, power:{phase["power"]}, frequency:{data["frequency"]}', end='') @@ -185,17 +189,17 @@ if __name__ == '__main__': # Load ahoy.yml config file try: - if isinstance(global_config.config_file, str) == True: - with open(global_config.config_file, 'r') as yf: - cfg = yaml.load(yf, Loader=SafeLoader) + if isinstance(global_config.config_file, str): + with open(global_config.config_file, 'r') as fh_yaml: + cfg = yaml.load(fh_yaml, Loader=SafeLoader) else: - with open('ahoy.yml', 'r') as yf: - cfg = yaml.load(yf, Loader=SafeLoader) + with open('ahoy.yml', 'r') as fh_yaml: + cfg = yaml.load(fh_yaml, Loader=SafeLoader) except FileNotFoundError: print("Could not load config file. Try --help") sys.exit(2) - except yaml.YAMLError as ye: - print('Failed to load config frile {global_config.config_file}: {ye}') + except yaml.YAMLError as e_yaml: + print('Failed to load config frile {global_config.config_file}: {e_yaml}') sys.exit(1) ahoy_config = dict(cfg.get('ahoy', {})) @@ -241,18 +245,18 @@ if __name__ == '__main__': if not radio.begin(): raise RuntimeError('Can\'t open radio') - inverters = [inverter.get('serial') for inverter in ahoy_config.get('inverters', [])] - for inverter in ahoy_config.get('inverters', []): - inverter_ser = inverter.get('serial') - command_queue[str(inverter_ser)] = [] + 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') + command_queue[str(g_inverter_ser)] = [] # # Enables and subscribe inverter to mqtt /command-Topic # - if mqtt_client and inverter.get('mqtt', {}).get('send_raw_enabled', False): + if mqtt_client and g_inverter.get('mqtt', {}).get('send_raw_enabled', False): topic_item = ( - str(inverter_ser), - inverter.get('mqtt', {}).get('topic', f'hoymiles/{inverter_ser}') + '/command' + str(g_inverter_ser), + g_inverter.get('mqtt', {}).get('topic', f'hoymiles/{g_inverter_ser}') + '/command' ) mqtt_client.subscribe(topic_item[1]) mqtt_command_topic_subs.append(topic_item) diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index cd0e87a0..59bc2292 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -1,12 +1,18 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- + +""" +Hoymiles Micro-Inverters decoder library +""" + import struct -import crcmod from datetime import datetime, timedelta +import crcmod f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') class Response: + """ All Response Shared methods """ inverter_ser = None inverter_name = None dtu_ser = None @@ -28,6 +34,7 @@ class Response: self.time_rx = datetime.now() def __dict__(self): + """ Base values, availabe in each __dict__ call """ return { 'inverter_ser': self.inverter_ser, 'inverter_name': self.inverter_name, @@ -36,9 +43,8 @@ class Response: class StatusResponse(Response): """Inverter StatusResponse object""" e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor'] - - def __init__(self, *args, **params): - super().__init__(*args, **params) + temperature = None + frequency = None def unpack(self, fmt, base): """ @@ -120,9 +126,6 @@ class UnknownResponse(Response): Debugging helper for unknown payload format """ - def __init__(self, *args, **params): - super().__init__(*args, **params) - @property def hex_ascii(self): """ @@ -131,7 +134,7 @@ class UnknownResponse(Response): :return: hexlifierd byte string :rtype: str """ - return ' '.join([f'{b:02x}' for b in self.response]) + return ' '.join([f'{byte:02x}' for byte in self.response]) @property def valid_crc(self): @@ -153,13 +156,13 @@ class UnknownResponse(Response): res = self.response - r = len(res) % 16 - res = res[:r*-1] + rem = len(res) % 16 + res = res[:rem*-1] vals = None if len(res) % 16 == 0: - n = len(res)/4 - vals = struct.unpack(f'>{int(n)}L', res) + rlen = len(res)/4 + vals = struct.unpack(f'>{int(rlen)}L', res) return vals @@ -171,13 +174,13 @@ class UnknownResponse(Response): res = self.response[2:] - r = len(res) % 16 - res = res[:r*-1] + rem = len(res) % 16 + res = res[:rem*-1] vals = None if len(res) % 16 == 0: - n = len(res)/4 - vals = struct.unpack(f'>{int(n)}L', res) + rlen = len(res)/4 + vals = struct.unpack(f'>{int(rlen)}L', res) return vals @@ -189,13 +192,13 @@ class UnknownResponse(Response): res = self.response[4:] - r = len(res) % 16 - res = res[:r*-1] + rem = len(res) % 16 + res = res[:rem*-1] vals = None if len(res) % 16 == 0: - n = len(res)/4 - vals = struct.unpack(f'>{int(n)}L', res) + rlen = len(res)/4 + vals = struct.unpack(f'>{int(rlen)}L', res) return vals @@ -207,13 +210,13 @@ class UnknownResponse(Response): res = self.response[6:] - r = len(res) % 16 - res = res[:r*-1] + rem = len(res) % 16 + res = res[:rem*-1] vals = None if len(res) % 16 == 0: - n = len(res)/4 - vals = struct.unpack(f'>{int(n)}L', res) + rlen = len(res)/4 + vals = struct.unpack(f'>{int(rlen)}L', res) return vals @@ -225,13 +228,13 @@ class UnknownResponse(Response): res = self.response - r = len(res) % 4 - res = res[:r*-1] + rem = len(res) % 4 + res = res[:rem*-1] vals = None if len(res) % 4 == 0: - n = len(res)/2 - vals = struct.unpack(f'>{int(n)}H', res) + rlen = len(res)/2 + vals = struct.unpack(f'>{int(rlen)}H', res) return vals @@ -243,17 +246,18 @@ class UnknownResponse(Response): res = self.response[1:] - r = len(res) % 4 - res = res[:r*-1] + rem = len(res) % 4 + res = res[:rem*-1] vals = None if len(res) % 4 == 0: - n = len(res)/2 - vals = struct.unpack(f'>{int(n)}H', res) + rlen = len(res)/2 + vals = struct.unpack(f'>{int(rlen)}H', res) return vals class EventsResponse(UnknownResponse): + """ Hoymiles micro-inverter event log decode helper """ alarm_codes = { 1: 'Inverter start', @@ -337,10 +341,10 @@ class EventsResponse(UnknownResponse): status = self.response[:2] chunk_size = 12 - for c in range(2, len(self.response), chunk_size): - chunk = self.response[c:c+chunk_size] + for i_chunk in range(2, len(self.response), chunk_size): + chunk = self.response[i_chunk:i_chunk+chunk_size] - print(' '.join([f'{b:02x}' for b in chunk]) + ': ') + print(' '.join([f'{byte:02x}' for byte in chunk]) + ': ') opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6]) a_text = self.alarm_codes.get(a_code, 'N/A') @@ -351,7 +355,7 @@ class EventsResponse(UnknownResponse): print(f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk))) print(end='', flush=True) -class DEBUG_DecodeAny(UnknownResponse): +class DebugDecodeAny(UnknownResponse): """Default decoder""" def __init__(self, *args, **params): @@ -415,206 +419,247 @@ class DEBUG_DecodeAny(UnknownResponse): # 1121-Series Intervers, 1 MPPT, 1 Phase -class HM300_Decode0B(StatusResponse): - def __init__(self, *args, **params): - super().__init__(*args, **params) +class Hm300Decode0B(StatusResponse): + """ 1121-series mirco-inverters status data """ @property def dc_voltage_0(self): + """ String 1 VDC """ return self.unpack('>H', 2)[0]/10 @property def dc_current_0(self): + """ String 1 ampere """ return self.unpack('>H', 4)[0]/100 @property def dc_power_0(self): + """ String 1 watts """ return self.unpack('>H', 6)[0]/10 @property def dc_energy_total_0(self): + """ String 1 total energy in Wh """ return self.unpack('>L', 8)[0] @property def dc_energy_daily_0(self): + """ String 1 daily energy in Wh """ return self.unpack('>H', 12)[0] - @property def ac_voltage_0(self): + """ Phase 1 VAC """ return self.unpack('>H', 14)[0]/10 @property def ac_current_0(self): + """ Phase 1 ampere """ return self.unpack('>H', 22)[0]/100 @property def ac_power_0(self): + """ Phase 1 watts """ return self.unpack('>H', 18)[0]/10 @property def frequency(self): + """ Grid frequency in Hertz """ return self.unpack('>H', 16)[0]/100 @property def temperature(self): + """ Inverter temperature in °C """ return self.unpack('>H', 26)[0]/10 -class HM300_Decode11(EventsResponse): - def __init__(self, *args, **params): - super().__init__(*args, **params) +class Hm300Decode11(EventsResponse): + """ Inverter generic events log """ -class HM300_Decode12(EventsResponse): - def __init__(self, *args, **params): - super().__init__(*args, **params) +class Hm300Decode12(EventsResponse): + """ Inverter major events log """ # 1141-Series Inverters, 2 MPPT, 1 Phase -class HM600_Decode0B(StatusResponse): - def __init__(self, *args, **params): - super().__init__(*args, **params) +class Hm600Decode0B(StatusResponse): + """ 1141-series mirco-inverters status data """ @property def dc_voltage_0(self): + """ String 1 VDC """ return self.unpack('>H', 2)[0]/10 @property def dc_current_0(self): + """ String 1 ampere """ return self.unpack('>H', 4)[0]/100 @property def dc_power_0(self): + """ String 1 watts """ return self.unpack('>H', 6)[0]/10 @property def dc_energy_total_0(self): + """ String 1 total energy in Wh """ return self.unpack('>L', 14)[0] @property def dc_energy_daily_0(self): + """ String 1 daily energy in Wh """ return self.unpack('>H', 22)[0] @property def dc_voltage_1(self): + """ String 2 VDC """ return self.unpack('>H', 8)[0]/10 @property def dc_current_1(self): + """ String 2 ampere """ return self.unpack('>H', 10)[0]/100 @property def dc_power_1(self): + """ String 2 watts """ return self.unpack('>H', 12)[0]/10 @property def dc_energy_total_1(self): + """ String 2 total energy in Wh """ return self.unpack('>L', 18)[0] @property def dc_energy_daily_1(self): + """ String 2 daily energy in Wh """ return self.unpack('>H', 24)[0] @property def ac_voltage_0(self): + """ Phase 1 VAC """ return self.unpack('>H', 26)[0]/10 @property def ac_current_0(self): + """ Phase 1 ampere """ return self.unpack('>H', 34)[0]/10 @property def ac_power_0(self): + """ Phase 1 watts """ return self.unpack('>H', 30)[0]/10 @property def frequency(self): + """ Grid frequency in Hertz """ return self.unpack('>H', 28)[0]/100 @property def temperature(self): + """ Inverter temperature in °C """ return self.unpack('>H', 38)[0]/10 @property def alarm_count(self): + """ Event counter """ return self.unpack('>H', 40)[0] -class HM600_Decode11(EventsResponse): - def __init__(self, *args, **params): - super().__init__(*args, **params) +class Hm600Decode11(EventsResponse): + """ Inverter generic events log """ -class HM600_Decode12(EventsResponse): - def __init__(self, *args, **params): - super().__init__(*args, **params) +class Hm600Decode12(EventsResponse): + """ Inverter major events log """ # 1161-Series Inverters, 4 MPPT, 1 Phase -class HM1200_Decode0B(StatusResponse): - def __init__(self, *args, **params): - super().__init__(*args, **params) +class Hm1200Decode0B(StatusResponse): + """ 1161-series mirco-inverters status data """ @property def dc_voltage_0(self): + """ String 1 VDC """ return self.unpack('>H', 2)[0]/10 @property def dc_current_0(self): + """ String 1 ampere """ return self.unpack('>H', 4)[0]/100 @property def dc_power_0(self): + """ String 1 watts """ return self.unpack('>H', 8)[0]/10 @property def dc_energy_total_0(self): + """ String 1 total energy in Wh """ return self.unpack('>L', 12)[0] @property def dc_energy_daily_0(self): + """ String 1 daily energy in Wh """ return self.unpack('>H', 20)[0] @property def dc_voltage_1(self): + """ String 2 VDC """ return self.unpack('>H', 2)[0]/10 @property def dc_current_1(self): + """ String 2 ampere """ return self.unpack('>H', 4)[0]/100 @property def dc_power_1(self): + """ String 2 watts """ return self.unpack('>H', 10)[0]/10 @property def dc_energy_total_1(self): + """ String 2 total energy in Wh """ return self.unpack('>L', 16)[0] @property def dc_energy_daily_1(self): + """ String 2 daily energy in Wh """ return self.unpack('>H', 22)[0] @property def dc_voltage_2(self): + """ String 3 VDC """ return self.unpack('>H', 24)[0]/10 @property def dc_current_2(self): + """ String 3 ampere """ return self.unpack('>H', 26)[0]/100 @property def dc_power_2(self): + """ String 3 watts """ return self.unpack('>H', 30)[0]/10 @property def dc_energy_total_2(self): + """ String 3 total energy in Wh """ return self.unpack('>L', 34)[0] @property def dc_energy_daily_2(self): + """ String 3 daily energy in Wh """ return self.unpack('>H', 42)[0] @property def dc_voltage_3(self): + """ String 4 VDC """ return self.unpack('>H', 24)[0]/10 @property def dc_current_3(self): + """ String 4 ampere """ return self.unpack('>H', 28)[0]/100 @property def dc_power_3(self): + """ String 4 watts """ return self.unpack('>H', 32)[0]/10 @property def dc_energy_total_3(self): + """ String 4 total energy in Wh """ return self.unpack('>L', 38)[0] @property def dc_energy_daily_3(self): + """ String 4 daily energy in Wh """ return self.unpack('>H', 44)[0] @property def ac_voltage_0(self): + """ Phase 1 VAC """ return self.unpack('>H', 46)[0]/10 @property def ac_current_0(self): + """ Phase 1 ampere """ return self.unpack('>H', 54)[0]/100 @property def ac_power_0(self): + """ Phase 1 watts """ return self.unpack('>H', 50)[0]/10 @property def frequency(self): + """ Grid frequency in Hertz """ return self.unpack('>H', 48)[0]/100 @property def temperature(self): + """ Inverter temperature in °C """ return self.unpack('>H', 58)[0]/10 -class HM1200_Decode11(EventsResponse): - def __init__(self, *args, **params): - super().__init__(*args, **params) +class Hm1200Decode11(EventsResponse): + """ Inverter generic events log """ -class HM1200_Decode12(EventsResponse): - def __init__(self, *args, **params): - super().__init__(*args, **params) +class Hm1200Decode12(EventsResponse): + """ Inverter major events log """ diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index f1b6a2d1..7b942846 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -1,4 +1,9 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Hoymiles output plugin library +""" import socket from datetime import datetime, timezone @@ -11,15 +16,46 @@ except ModuleNotFoundError: class OutputPluginFactory: def __init__(self, **params): - """Initialize output plugin""" + """ + Initialize output plugin - self.inverter_ser = params.get('inverter_ser', 0) + :param inverter_ser: The inverter serial + :type inverter_ser: str + :param inverter_name: The configured name for the inverter + :type inverter_name: str + """ - def store_status(self, data): + self.inverter_ser = params.get('inverter_ser', '') + self.inverter_name = params.get('inverter_name', None) + + def store_status(self, response, **params): + """ + Default function + + :raises NotImplementedError: when the plugin does not implement store status data + """ raise NotImplementedError('The current output plugin does not implement store_status') class InfluxOutputPlugin(OutputPluginFactory): + """ Influx2 output plugin """ + api = None + def __init__(self, url, token, **params): + """ + Initialize InfluxOutputPlugin + + The following targets must be present in your InfluxDB. This does not + automatically create anything for You. + + :param str url: The url to connect this client to. Like http://localhost:8086 + :param str token: Influx2 access token which is allowed to write to bucket + :param org: Influx2 org, the token belongs to + :type org: str + :param bucket: Influx2 bucket to store data in (also known as retention policy) + :type bucket: str + :param measurement: Default measurement-prefix to use + :type measurement: str + """ super().__init__(**params) self._bucket = params.get('bucket', 'hoymiles/autogen') @@ -30,20 +66,20 @@ class InfluxOutputPlugin(OutputPluginFactory): client = InfluxDBClient(url, token, bucket=self._bucket) self.api = client.write_api() - def store_status(self, response): + def store_status(self, response, **params): """ Publish StatusResponse object - :param influxdb.InfluxDBClient influx_client: A connected instance to Influx database - :param str inverter_ser: inverter serial - :param hoymiles.StatusResponse data: decoded inverter StatusResponse - :type response: hoymiles.StatusResponse - :param measurement: Influx measurement name - :type measurement: str + :param hoymiles.decoders.StatusResponse response: StatusResponse object + :type response: hoymiles.decoders.StatusResponse + :param measurement: Custom influx measurement name + :type measurement: str or None + + :raises ValueError: when response is not instance of StatusResponse """ if not isinstance(response, StatusResponse): - raise RuntimeError('Data needs to be instance of StatusResponse') + raise ValueError('Data needs to be instance of StatusResponse') data = response.__dict__() @@ -89,7 +125,30 @@ except ModuleNotFoundError: pass class MqttOutputPlugin(OutputPluginFactory): + """ Mqtt output plugin """ + client = None + def __init__(self, *args, **params): + """ + Initialize MqttOutputPlugin + + :param host: Broker ip or hostname (defaults to: 127.0.0.1) + :type host: str + :param port: Broker port + :type port: int (defaults to: 1883) + :param user: Optional username to login to the broker + :type user: str or None + :param password: Optional passwort to login to the broker + :type password: str or None + :param topic: Topic prefix to use (defaults to: hoymiles/{inverter_ser}) + :type topic: str + + :param paho.mqtt.client.Client broker: mqtt-client instance + :param str inverter_ser: inverter serial + :param hoymiles.StatusResponse data: decoded inverter StatusResponse + :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) + :type topic: str + """ super().__init__(*args, **params) mqtt_client = paho.mqtt.client.Client() @@ -103,36 +162,36 @@ class MqttOutputPlugin(OutputPluginFactory): """ Publish StatusResponse object - :param paho.mqtt.client.Client broker: mqtt-client instance - :param str inverter_ser: inverter serial - :param hoymiles.StatusResponse data: decoded inverter StatusResponse + :param hoymiles.decoders.StatusResponse response: StatusResponse object :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) :type topic: str + + :raises ValueError: when response is not instance of StatusResponse """ if not isinstance(response, StatusResponse): - raise RuntimeError('Data needs to be instance of StatusResponse') + raise ValueError('Data needs to be instance of StatusResponse') data = response.__dict__() - topic = params.get('topic', f'hoymiles/{inverter_ser}') + topic = params.get('topic', f'hoymiles/{data["inverter_ser"]}') # AC Data phase_id = 0 for phase in data['phases']: - self.mqtt_client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) - self.mqtt_client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) - self.mqtt_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}/voltage', phase['voltage']) + self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) phase_id = phase_id + 1 # DC Data string_id = 0 for string in data['strings']: - self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000) - self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) - self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) - self.mqtt_client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) + self.client.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000) + self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) + self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) + self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) string_id = string_id + 1 # Global - self.mqtt_client.publish(f'{topic}/frequency', data['frequency']) - self.mqtt_client.publish(f'{topic}/temperature', data['temperature']) + self.client.publish(f'{topic}/frequency', data['frequency']) + self.client.publish(f'{topic}/temperature', data['temperature']) From e473583a5536b4f8ffb7099d3510912db84928a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Jonas=20S=C3=A4mann?= Date: Mon, 16 May 2022 13:10:09 +0200 Subject: [PATCH 06/10] Change alarm code 2 Seems that alarm code 2 appears on failed commands --- tools/rpi/hoymiles/decoders/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index 59bc2292..677d5a65 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -261,7 +261,7 @@ class EventsResponse(UnknownResponse): alarm_codes = { 1: 'Inverter start', - 2: 'Producing power', + 2: 'DTU command failed', 121: 'Over temperature protection', 125: 'Grid configuration parameter error', 126: 'Software error code 126', From 724f0ab83aa32c489dc6febee0ba29c3aa3f186d Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 17 May 2022 15:30:36 +0200 Subject: [PATCH 07/10] * complete payload processed (and crc checked) * inverter type is defined by serial number * serial debug can be switched live (using setup) -> Note: only one inverter is supported for now! --- tools/esp8266/app.cpp | 163 +++++++++++++++++++++++++---------- tools/esp8266/app.h | 6 ++ tools/esp8266/config.h | 3 + tools/esp8266/crc.cpp | 4 +- tools/esp8266/crc.h | 2 +- tools/esp8266/defines.h | 6 +- tools/esp8266/hmDefines.h | 168 ++++++++++++++++--------------------- tools/esp8266/hmInverter.h | 43 ++++------ tools/esp8266/hmRadio.h | 59 +++++-------- tools/esp8266/hmSystem.h | 16 +++- 10 files changed, 261 insertions(+), 209 deletions(-) diff --git a/tools/esp8266/app.cpp b/tools/esp8266/app.cpp index 7eee08bf..472aeeb3 100644 --- a/tools/esp8266/app.cpp +++ b/tools/esp8266/app.cpp @@ -25,6 +25,11 @@ app::app() : Main() { memset(mPacketIds, 0, sizeof(uint32_t)*DBG_CMD_LIST_LEN); + memset(mPayload, 0, (MAX_PAYLOAD_ENTRIES * MAX_RF_PAYLOAD_SIZE)); + memset(mPayloadLen, 0, MAX_PAYLOAD_ENTRIES); + mPayloadComplete = true; + mMaxPackId = 0; + mSys = new HmSystemType(); } @@ -50,21 +55,20 @@ void app::setup(uint32_t timeout) { if(mSettingsValid) { uint64_t invSerial; char invName[MAX_NAME_LENGTH + 1] = {0}; - uint8_t invType; // inverter for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { mEep->read(ADDR_INV_ADDR + (i * 8), &invSerial); mEep->read(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), invName, MAX_NAME_LENGTH); - mEep->read(ADDR_INV_TYPE + i, &invType); if(0ULL != invSerial) { - mSys->addInverter(invName, invSerial, invType); - DPRINTLN("add inverter: " + String(invName) + ", SN: " + String(invSerial, HEX) + ", type: " + String(invType)); + mSys->addInverter(invName, invSerial); + DPRINTLN("add inverter: " + String(invName) + ", SN: " + String(invSerial, HEX)); } } mEep->read(ADDR_INV_INTERVAL, &mSendInterval); if(mSendInterval < 5) mSendInterval = 5; + mSendTicker = mSendInterval; // pinout mEep->read(ADDR_PINOUT, &mSys->Radio.pinCs); @@ -84,6 +88,7 @@ void app::setup(uint32_t timeout) { mSerialDebug = (tmp == 0x01); if(mSerialInterval < 1) mSerialInterval = 1; + mSys->Radio.mSerialDebug = mSerialDebug; // mqtt @@ -134,31 +139,39 @@ void app::loop(void) { Main::loop(); if(checkTicker(&mRxTicker, 5)) { - mSys->Radio.switchRxCh(); + bool rcvRdy = mSys->Radio.switchRxCh(); + if(!mSys->BufCtrl.empty()) { - uint8_t len, rptCnt; + uint8_t len; packet_t *p = mSys->BufCtrl.getBack(); - //if(mSerialDebug) - // mSys->Radio.dumpBuf("RAW ", p->packet, MAX_RF_PAYLOAD_SIZE); - - if(mSys->Radio.checkPaketCrc(p->packet, &len, &rptCnt, p->rxCh)) { + if(mSys->Radio.checkPaketCrc(p->packet, &len, p->rxCh)) { // process buffer only on first occurrence - if((0 != len) && (0 == rptCnt)) { - uint8_t *packetId = &p->packet[9]; - //DPRINTLN("CMD " + String(*packetId, HEX)); - if(mSerialDebug) - mSys->Radio.dumpBuf("Payload ", p->packet, len); + if(mSerialDebug) { + DPRINT("Received " + String(len) + " bytes channel " + String(p->rxCh) + ": "); + mSys->Radio.dumpBuf(NULL, p->packet, len); + } - Inverter<> *iv = mSys->findInverter(&p->packet[1]); + if(0 != len) { + uint8_t *packetId = &p->packet[9]; + if((*packetId & 0x7F) < 5) { + memcpy(mPayload[(*packetId & 0x7F) - 1], &p->packet[10], len-11); + mPayloadLen[(*packetId & 0x7F) - 1] = len-11; + } + + if((*packetId & 0x80) == 0x80) { + if((*packetId & 0x7f) > mMaxPackId) + mMaxPackId = (*packetId & 0x7f); + } + + /*Inverter<> *iv = mSys->findInverter(&p->packet[1]); if(NULL != iv) { for(uint8_t i = 0; i < iv->listLen; i++) { if(iv->assign[i].cmdId == *packetId) iv->addValue(i, &p->packet[9]); } iv->doCalculations(); - //memcpy(mPayload[(*packetId & 0x7F) - 1], &p->packet[9], MAX_RF_PAYLOAD_SIZE - 11); - } + }*/ if(*packetId == 0x01) mPacketIds[0]++; else if(*packetId == 0x02) mPacketIds[1]++; @@ -170,8 +183,49 @@ void app::loop(void) { else mPacketIds[7]++; } } + mSys->BufCtrl.popBack(); } + + + // TODO: support more than one inverter! + if(rcvRdy && (!mPayloadComplete)) { + Inverter<> *iv = mSys->getInverterByPos(0); + if(!buildPayload()) { + if(mMaxPackId != 0) { + for(uint8_t i = 0; i < (mMaxPackId-1); i ++) { + // retransmit + if(mPayloadLen[i] == 0) { + if(mSerialDebug) + DPRINTLN("Error while retrieving data: Frame " + String(i+1) + " missing: Request Retransmit"); + mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x15, (0x81+i), true); + } + } + } + mSys->Radio.switchRxCh(200); + } + else { + mPayloadComplete = true; + uint8_t payload[256] = {0}; + uint8_t offs = 0; + for(uint8_t i = 0; i < (mMaxPackId); i ++) { + memcpy(&payload[offs], mPayload[i], (mPayloadLen[i])); + offs += (mPayloadLen[i]); + } + offs-=2; + if(mSerialDebug) { + DPRINT("Payload (" + String(offs) + "): "); + mSys->Radio.dumpBuf(NULL, payload, offs); + } + + if(NULL != iv) { + for(uint8_t i = 0; i < iv->listLen; i++) { + iv->addValue(i, payload); + } + iv->doCalculations(); + } + } + } } if(checkTicker(&mTicker, 1000)) { @@ -220,8 +274,18 @@ void app::loop(void) { if(++mSendTicker >= mSendInterval) { mSendTicker = 0; - if(!mSys->BufCtrl.empty()) - DPRINTLN("recbuf not empty! #" + String(mSys->BufCtrl.getFill())); + memset(mPayloadLen, 0, MAX_PAYLOAD_ENTRIES); + mMaxPackId = 0; + if(mSerialDebug) { + if(!mPayloadComplete) + DPRINTLN("no Payload received!"); + } + mPayloadComplete = false; + + if(!mSys->BufCtrl.empty()) { + if(mSerialDebug) + DPRINTLN("recbuf not empty! #" + String(mSys->BufCtrl.getFill())); + } Inverter<> *inv; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { inv = mSys->getInverterByPos(i); @@ -242,6 +306,30 @@ void app::handleIntr(void) { } +//----------------------------------------------------------------------------- +bool app::buildPayload() { + //DPRINTLN("Payload"); + uint16_t crc = 0xffff, crcRcv; + if(mMaxPackId > MAX_PAYLOAD_ENTRIES) + mMaxPackId = MAX_PAYLOAD_ENTRIES; + + for(uint8_t i = 0; i < mMaxPackId; i ++) { + if(mPayloadLen[i] > 0) { + if(i == (mMaxPackId-1)) { + crc = crc16(mPayload[i], mPayloadLen[i] - 2, crc); + crcRcv = (mPayload[i][mPayloadLen[i] - 2] << 8) + | (mPayload[i][mPayloadLen[i] - 1]); + } + else + crc = crc16(mPayload[i], mPayloadLen[i], crc); + } + } + if(crc == crcRcv) + return true; + return false; +} + + //----------------------------------------------------------------------------- void app::showIndex(void) { String html = FPSTR(index_html); @@ -276,7 +364,6 @@ void app::showSetup(void) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { mEep->read(ADDR_INV_ADDR + (i * 8), &invSerial); mEep->read(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), invName, MAX_NAME_LENGTH); - mEep->read(ADDR_INV_TYPE + i, &invType); inv += "

Inverter "+ String(i) + "

"; inv += ""; @@ -289,16 +376,6 @@ void app::showSetup(void) { inv += ""; - - inv += ""; - inv += ""; } html.replace("{INVERTERS}", String(inv)); @@ -371,7 +448,7 @@ void app::showSetup(void) { html.replace("{MQTT_PORT}", "1883"); html.replace("{MQTT_USER}", ""); html.replace("{MQTT_PWD}", ""); - html.replace("{MQTT_TOPIC}", "/inverter"); + html.replace("{MQTT_TOPIC}", "inverter"); html.replace("{MQTT_INTVL}", "10"); html.replace("{SER_INTVL}", "10"); @@ -440,10 +517,10 @@ void app::showLiveData(void) { #ifdef LIVEDATA_VISUALIZED uint8_t modNum, pos; switch(iv->type) { - default: modNum = 1; break; - case INV_TYPE_HM600: - case INV_TYPE_HM800: modNum = 2; break; - case INV_TYPE_HM1200: modNum = 4; break; + default: modNum = 1; break; + case INV_TYPE_1CH: + case INV_TYPE_2CH: modNum = 2; break; + case INV_TYPE_4CH: modNum = 4; break; } modHtml += "
"; @@ -524,11 +601,6 @@ void app::saveValues(bool webSend = true) { // name mWeb->arg("inv" + String(i) + "Name").toCharArray(buf, 20); mEep->write(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), buf, MAX_NAME_LENGTH); - - // type - mWeb->arg("inv" + String(i) + "Type").toCharArray(buf, 20); - uint8_t type = atoi(buf); - mEep->write(ADDR_INV_TYPE + i, type); } interval = mWeb->arg("invInterval").toInt(); @@ -578,15 +650,18 @@ void app::saveValues(bool webSend = true) { mEep->write(ADDR_SER_INTERVAL, interval); tmp = (mWeb->arg("serEn") == "on"); mEep->write(ADDR_SER_ENABLE, (uint8_t)((tmp) ? 0x01 : 0x00)); - tmp = (mWeb->arg("serDbg") == "on"); - mEep->write(ADDR_SER_DEBUG, (uint8_t)((tmp) ? 0x01 : 0x00)); + mSerialDebug = (mWeb->arg("serDbg") == "on"); + mEep->write(ADDR_SER_DEBUG, (uint8_t)((mSerialDebug) ? 0x01 : 0x00)); + DPRINT("Info: Serial debug is "); + if(mSerialDebug) DPRINTLN("on"); else DPRINTLN("off"); + mSys->Radio.mSerialDebug = mSerialDebug; updateCrc(); if((mWeb->arg("reboot") == "on")) showReboot(); else { mShowRebootRequest = true; - mWeb->send(200, "text/html", "Setup saved" + mWeb->send(200, "text/html", "Setup saved" "

saved

"); } } diff --git a/tools/esp8266/app.h b/tools/esp8266/app.h index 26efc6e7..e1047a80 100644 --- a/tools/esp8266/app.h +++ b/tools/esp8266/app.h @@ -40,6 +40,8 @@ class app : public Main { } private: + bool buildPayload(); + void showIndex(void); void showSetup(void); void showSave(void); @@ -75,6 +77,10 @@ class app : public Main { uint32_t mPacketIds[DBG_CMD_LIST_LEN+1]; uint32_t mRecCnt; + uint8_t mPayload[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; + uint8_t mPayloadLen[MAX_PAYLOAD_ENTRIES]; + bool mPayloadComplete; + uint8_t mMaxPackId; // timer uint32_t mTicker; diff --git a/tools/esp8266/config.h b/tools/esp8266/config.h index c37f979a..e96ab5cb 100644 --- a/tools/esp8266/config.h +++ b/tools/esp8266/config.h @@ -38,6 +38,9 @@ // maximum buffer length of packet received / sent to RF24 module #define MAX_RF_PAYLOAD_SIZE 32 +// maximum total payload size +#define MAX_PAYLOAD_ENTRIES 4 + // changes the style of "/setup" page, visualized = nicer #define LIVEDATA_VISUALIZED diff --git a/tools/esp8266/crc.cpp b/tools/esp8266/crc.cpp index be2d8a57..7694dadd 100644 --- a/tools/esp8266/crc.cpp +++ b/tools/esp8266/crc.cpp @@ -11,8 +11,8 @@ uint8_t crc8(uint8_t buf[], uint8_t len) { return crc; } -uint16_t crc16(uint8_t buf[], uint8_t len) { - uint16_t crc = 0xffff; +uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start) { + uint16_t crc = start; uint8_t shift = 0; for(uint8_t i = 0; i < len; i ++) { diff --git a/tools/esp8266/crc.h b/tools/esp8266/crc.h index 90baa548..349146b3 100644 --- a/tools/esp8266/crc.h +++ b/tools/esp8266/crc.h @@ -10,7 +10,7 @@ #define CRC16_NRF24_POLYNOM 0x1021 uint8_t crc8(uint8_t buf[], uint8_t len); -uint16_t crc16(uint8_t buf[], uint8_t len); +uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start = 0xffff); uint16_t crc16nrf24(uint8_t buf[], uint16_t lenBits, uint16_t startBit = 0, uint16_t crcIn = 0xffff); #endif /*__CRC_H__*/ diff --git a/tools/esp8266/defines.h b/tools/esp8266/defines.h index 459125a8..bcad6275 100644 --- a/tools/esp8266/defines.h +++ b/tools/esp8266/defines.h @@ -15,8 +15,8 @@ // VERSION //------------------------------------- #define VERSION_MAJOR 0 -#define VERSION_MINOR 3 -#define VERSION_PATCH 9 +#define VERSION_MINOR 4 +#define VERSION_PATCH 0 //------------------------------------- @@ -68,7 +68,7 @@ typedef struct { #define ADDR_INV_ADDR ADDR_RF24_AMP_PWR + RF24_AMP_PWR_LEN #define ADDR_INV_NAME ADDR_INV_ADDR + INV_ADDR_LEN -#define ADDR_INV_TYPE ADDR_INV_NAME + INV_NAME_LEN +#define ADDR_INV_TYPE ADDR_INV_NAME + INV_NAME_LEN // obsolete #define ADDR_INV_INTERVAL ADDR_INV_TYPE + INV_TYPE_LEN #define ADDR_MQTT_ADDR ADDR_INV_INTERVAL + INV_INTERVAL_LEN diff --git a/tools/esp8266/hmDefines.h b/tools/esp8266/hmDefines.h index 1c27ced3..1f8c9389 100644 --- a/tools/esp8266/hmDefines.h +++ b/tools/esp8266/hmDefines.h @@ -25,26 +25,22 @@ const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", " // indices to calculation functions, defined in hmInverter.h enum {CALC_YT_CH0 = 0, CALC_YD_CH0, CALC_UDC_CH}; +enum {CMD_CALC = 0xffff}; // CH0 is default channel (freq, ac, temp) enum {CH0 = 0, CH1, CH2, CH3, CH4}; -// received command ids, special command CMDFF for calculations -enum {CMD01 = 0x01, CMD02, CMD03, CMD82 = 0x82, CMD83, CMD84, CMDFF=0xff}; -enum {INV_TYPE_HM600 = 0, INV_TYPE_HM1200, INV_TYPE_HM400, INV_TYPE_HM800}; -const char* const invTypes[] = {"HM600", "HM1200 / HM1500", "HM400", "HM800"}; -#define NUM_INVERTER_TYPES 4 +enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH}; typedef struct { uint8_t fieldId; // field id uint8_t unitId; // uint id - uint8_t ch; // channel 0 - 3 - uint8_t cmdId; // received command id + uint8_t ch; // channel 0 - 4 uint8_t start; // pos of first byte in buffer uint8_t num; // number of bytes in buffer - uint16_t div; // divisor + uint16_t div; // divisor / calc command } byteAssign_t; @@ -54,106 +50,88 @@ typedef struct { * */ //------------------------------------- -// HM400 HM350?, HM300? +// HM300, HM350, HM400 //------------------------------------- -const byteAssign_t hm400assignment[] = { - { FLD_UDC, UNIT_V, CH1, CMD01, 3, 2, 10 }, - { FLD_IDC, UNIT_A, CH1, CMD01, 5, 2, 100 }, - { FLD_PDC, UNIT_W, CH1, CMD01, 7, 2, 10 }, - { FLD_YT, UNIT_KWH, CH1, CMD01, 9, 4, 1000 }, - { FLD_YD, UNIT_WH, CH1, CMD01, 13, 2, 1 }, - { FLD_UAC, UNIT_V, CH0, CMD01, 15, 2, 10 }, - { FLD_F, UNIT_HZ, CH0, CMD82, 1, 2, 100 }, - { FLD_PAC, UNIT_W, CH0, CMD82, 3, 2, 10 }, - { FLD_IAC, UNIT_A, CH0, CMD82, 7, 2, 100 }, - { FLD_T, UNIT_C, CH0, CMD82, 11, 2, 10 } +const byteAssign_t hm1chAssignment[] = { + { FLD_UDC, UNIT_V, CH1, 2, 2, 10 }, + { FLD_IDC, UNIT_A, CH1, 4, 2, 100 }, + { FLD_PDC, UNIT_W, CH1, 6, 2, 10 }, + { FLD_YD, UNIT_WH, CH1, 12, 2, 1 }, + { FLD_YT, UNIT_KWH, CH1, 8, 4, 1000 }, + + { FLD_UAC, UNIT_V, CH0, 14, 2, 10 }, + { FLD_IAC, UNIT_A, CH0, 22, 2, 100 }, + { FLD_PAC, UNIT_W, CH0, 18, 2, 10 }, + { FLD_F, UNIT_HZ, CH0, 16, 2, 100 }, + { FLD_T, UNIT_C, CH0, 26, 2, 10 } }; -#define HM400_LIST_LEN (sizeof(hm400assignment) / sizeof(byteAssign_t)) +#define HM1CH_LIST_LEN (sizeof(hm1chAssignment) / sizeof(byteAssign_t)) //------------------------------------- -// HM600, HM700 +// HM600, HM700, HM800 //------------------------------------- -const byteAssign_t hm600assignment[] = { - { FLD_UDC, UNIT_V, CH1, CMD01, 3, 2, 10 }, - { FLD_IDC, UNIT_A, CH1, CMD01, 5, 2, 100 }, - { FLD_PDC, UNIT_W, CH1, CMD01, 7, 2, 10 }, - { FLD_UDC, UNIT_V, CH2, CMD01, 9, 2, 10 }, - { FLD_IDC, UNIT_A, CH2, CMD01, 11, 2, 100 }, - { FLD_PDC, UNIT_W, CH2, CMD01, 13, 2, 10 }, - { FLD_YW, UNIT_WH, CH0, CMD02, 1, 2, 1 }, - { FLD_YT, UNIT_KWH, CH0, CMD02, 3, 4, 1000 }, - { FLD_YD, UNIT_WH, CH1, CMD02, 7, 2, 1 }, - { FLD_YD, UNIT_WH, CH2, CMD02, 9, 2, 1 }, - { FLD_UAC, UNIT_V, CH0, CMD02, 11, 2, 10 }, - { FLD_F, UNIT_HZ, CH0, CMD02, 13, 2, 100 }, - { FLD_PAC, UNIT_W, CH0, CMD02, 15, 2, 10 }, - { FLD_IAC, UNIT_A, CH0, CMD83, 3, 2, 100 }, - { FLD_T, UNIT_C, CH0, CMD83, 7, 2, 10 } +const byteAssign_t hm2chAssignment[] = { + { FLD_UDC, UNIT_V, CH1, 2, 2, 10 }, + { FLD_IDC, UNIT_A, CH1, 4, 2, 100 }, + { FLD_PDC, UNIT_W, CH1, 6, 2, 10 }, + { FLD_YD, UNIT_WH, CH1, 22, 2, 1 }, + { FLD_YD, UNIT_KWH, CH1, 14, 4, 1000 }, + + { FLD_UDC, UNIT_V, CH2, 8, 2, 10 }, + { FLD_IDC, UNIT_A, CH2, 10, 2, 100 }, + { FLD_PDC, UNIT_W, CH2, 12, 2, 10 }, + { FLD_YD, UNIT_WH, CH2, 18, 2, 1 }, + { FLD_YD, UNIT_KWH, CH2, 24, 4, 1000 }, + + { FLD_UAC, UNIT_V, CH0, 26, 2, 10 }, + { FLD_IAC, UNIT_A, CH0, 34, 2, 10 }, + { FLD_PAC, UNIT_W, CH0, 30, 2, 10 }, + { FLD_F, UNIT_HZ, CH0, 28, 2, 100 }, + { FLD_T, UNIT_C, CH0, 38, 2, 10 } + }; -#define HM600_LIST_LEN (sizeof(hm600assignment) / sizeof(byteAssign_t)) - - -//------------------------------------- -// HM800 -//------------------------------------- -const byteAssign_t hm800assignment[] = { - - { FLD_UDC, UNIT_V, CH1, CMD01, 3, 2, 10 }, - { FLD_IDC, UNIT_A, CH1, CMD01, 5, 2, 100 }, - { FLD_PDC, UNIT_W, CH1, CMD01, 7, 2, 10 }, - { FLD_UDC, UNIT_V, CH2, CMD01, 9, 2, 10 }, - { FLD_IDC, UNIT_A, CH2, CMD01, 11, 2, 100 }, - { FLD_PDC, UNIT_W, CH2, CMD01, 13, 2, 10 }, - { FLD_YW, UNIT_WH, CH0, CMD02, 1, 2, 1 }, - { FLD_YT, UNIT_KWH, CH0, CMD02, 3, 4, 1000 }, - { FLD_YD, UNIT_WH, CH1, CMD02, 7, 2, 1 }, - { FLD_YD, UNIT_WH, CH2, CMD02, 9, 2, 1 }, - { FLD_UAC, UNIT_V, CH0, CMD02, 11, 2, 10 }, - { FLD_F, UNIT_HZ, CH0, CMD02, 13, 2, 100 }, - { FLD_PAC, UNIT_W, CH0, CMD02, 15, 2, 10 }, - { FLD_IAC, UNIT_A, CH0, CMD83, 3, 2, 100 }, - { FLD_T, UNIT_C, CH0, CMD83, 7, 2, 10 } -}; -#define HM800_LIST_LEN (sizeof(hm800assignment) / sizeof(byteAssign_t)) +#define HM2CH_LIST_LEN (sizeof(hm2chAssignment) / sizeof(byteAssign_t)) //------------------------------------- // HM1200, HM1500 //------------------------------------- -const byteAssign_t hm1200assignment[] = { - { FLD_UDC, UNIT_V, CH1, CMD01, 3, 2, 10 }, - { FLD_IDC, UNIT_A, CH1, CMD01, 5, 2, 100 }, - { FLD_PDC, UNIT_W, CH1, CMD01, 9, 2, 10 }, - { FLD_YD, UNIT_WH, CH1, CMD02, 5, 2, 1 }, - { FLD_YT, UNIT_KWH, CH1, CMD01, 13, 4, 1000 }, - { FLD_UDC, UNIT_V, CH3, CMD02, 9, 2, 10 }, - { FLD_IDC, UNIT_A, CH2, CMD01, 7, 2, 100 }, - { FLD_PDC, UNIT_W, CH2, CMD01, 11, 2, 10 }, - { FLD_YD, UNIT_WH, CH2, CMD02, 7, 2, 1 }, - { FLD_YT, UNIT_KWH, CH2, CMD02, 1, 4, 1000 }, - { FLD_IDC, UNIT_A, CH3, CMD02, 11, 2, 100 }, - { FLD_PDC, UNIT_W, CH3, CMD02, 15, 2, 10 }, - { FLD_YD, UNIT_WH, CH3, CMD03, 11, 2, 1 }, - { FLD_YT, UNIT_KWH, CH3, CMD03, 3, 4, 1000 }, - { FLD_IDC, UNIT_A, CH4, CMD02, 13, 2, 100 }, - { FLD_PDC, UNIT_W, CH4, CMD03, 1, 2, 10 }, - { FLD_YD, UNIT_WH, CH4, CMD03, 13, 2, 1 }, - { FLD_YT, UNIT_KWH, CH4, CMD03, 7, 4, 1000 }, - { FLD_UAC, UNIT_V, CH0, CMD03, 15, 2, 10 }, - { FLD_IAC, UNIT_A, CH0, CMD84, 7, 2, 100 }, - { FLD_PAC, UNIT_W, CH0, CMD84, 3, 2, 10 }, - { FLD_F, UNIT_HZ, CH0, CMD84, 1, 2, 100 }, - { FLD_PCT, UNIT_PCT, CH0, CMD84, 9, 2, 10 }, - { FLD_T, UNIT_C, CH0, CMD84, 11, 2, 10 }, - { FLD_YD, UNIT_WH, CH0, CMDFF, CALC_YD_CH0, 0, 0 }, - { FLD_YT, UNIT_KWH, CH0, CMDFF, CALC_YT_CH0, 0, 0 }, - { FLD_UDC, UNIT_V, CH2, CMDFF, CALC_UDC_CH, CH1, 0 }, - { FLD_UDC, UNIT_V, CH4, CMDFF, CALC_UDC_CH, CH3, 0 } +const byteAssign_t hm4chAssignment[] = { + { FLD_UDC, UNIT_V, CH1, 2, 2, 10 }, + { FLD_IDC, UNIT_A, CH1, 4, 2, 100 }, + { FLD_PDC, UNIT_W, CH1, 8, 2, 10 }, + { FLD_YD, UNIT_WH, CH1, 20, 2, 1 }, + { FLD_YT, UNIT_KWH, CH1, 12, 4, 1000 }, + + { FLD_UDC, UNIT_V, CH2, CALC_UDC_CH, CH1, CMD_CALC }, + { FLD_IDC, UNIT_A, CH2, 6, 2, 100 }, + { FLD_PDC, UNIT_W, CH2, 10, 2, 10 }, + { FLD_YD, UNIT_WH, CH2, 22, 2, 1 }, + { FLD_YT, UNIT_KWH, CH2, 16, 4, 1000 }, + + { FLD_UDC, UNIT_V, CH3, 24, 2, 10 }, + { FLD_IDC, UNIT_A, CH3, 26, 2, 100 }, + { FLD_PDC, UNIT_W, CH3, 30, 2, 10 }, + { FLD_YD, UNIT_WH, CH3, 42, 2, 1 }, + { FLD_YT, UNIT_KWH, CH3, 34, 4, 1000 }, + + { FLD_UDC, UNIT_V, CH4, CALC_UDC_CH, CH3, CMD_CALC }, + { FLD_IDC, UNIT_A, CH4, 28, 2, 100 }, + { FLD_PDC, UNIT_W, CH4, 32, 2, 10 }, + { FLD_YD, UNIT_WH, CH4, 44, 2, 1 }, + { FLD_YT, UNIT_KWH, CH4, 38, 4, 1000 }, + + { FLD_UAC, UNIT_V, CH0, 46, 2, 10 }, + { FLD_IAC, UNIT_A, CH0, 54, 2, 100 }, + { FLD_PAC, UNIT_W, CH0, 50, 2, 10 }, + { FLD_F, UNIT_HZ, CH0, 48, 2, 100 }, + { FLD_PCT, UNIT_PCT, CH0, 56, 2, 10 }, + { FLD_T, UNIT_C, CH0, 58, 2, 10 }, + { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, + { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC } }; -#define HM1200_LIST_LEN (sizeof(hm1200assignment) / sizeof(byteAssign_t)) - - +#define HM4CH_LIST_LEN (sizeof(hm4chAssignment) / sizeof(byteAssign_t)) #endif /*__HM_DEFINES_H__*/ diff --git a/tools/esp8266/hmInverter.h b/tools/esp8266/hmInverter.h index f5e823e8..5b1ffd23 100644 --- a/tools/esp8266/hmInverter.h +++ b/tools/esp8266/hmInverter.h @@ -95,22 +95,20 @@ class Inverter { return assign[pos].ch; } - uint8_t getCmdId(uint8_t pos) { - return assign[pos].cmdId; - } - void addValue(uint8_t pos, uint8_t buf[]) { uint8_t ptr = assign[pos].start; uint8_t end = ptr + assign[pos].num; uint16_t div = assign[pos].div; - uint32_t val = 0; - do { - val <<= 8; - val |= buf[ptr]; - } while(++ptr != end); + if(CMD_CALC != div) { + uint32_t val = 0; + do { + val <<= 8; + val |= buf[ptr]; + } while(++ptr != end); - record[pos] = (RECORDTYPE)(val) / (RECORDTYPE)(div); + record[pos] = (RECORDTYPE)(val) / (RECORDTYPE)(div); + } } RECORDTYPE getValue(uint8_t pos) { @@ -119,7 +117,7 @@ class Inverter { void doCalculations(void) { for(uint8_t i = 0; i < listLen; i++) { - if(CMDFF == assign[i].cmdId) { + if(CMD_CALC == assign[i].div) { record[i] = calcFunctions[assign[i].start].func(this, assign[i].num); } } @@ -136,24 +134,19 @@ class Inverter { } void getAssignment(void) { - if(INV_TYPE_HM400 == type) { - listLen = (uint8_t)(HM400_LIST_LEN); - assign = (byteAssign_t*)hm400assignment; + if(INV_TYPE_1CH == type) { + listLen = (uint8_t)(HM1CH_LIST_LEN); + assign = (byteAssign_t*)hm1chAssignment; channels = 1; } - else if(INV_TYPE_HM600 == type) { - listLen = (uint8_t)(HM600_LIST_LEN); - assign = (byteAssign_t*)hm600assignment; + else if(INV_TYPE_2CH == type) { + listLen = (uint8_t)(HM2CH_LIST_LEN); + assign = (byteAssign_t*)hm2chAssignment; channels = 2; } - else if(INV_TYPE_HM800 == type) { - listLen = (uint8_t)(HM800_LIST_LEN); - assign = (byteAssign_t*)hm800assignment; - channels = 2; - } - else if(INV_TYPE_HM1200 == type) { - listLen = (uint8_t)(HM1200_LIST_LEN); - assign = (byteAssign_t*)hm1200assignment; + else if(INV_TYPE_4CH == type) { + listLen = (uint8_t)(HM4CH_LIST_LEN); + assign = (byteAssign_t*)hm4chAssignment; channels = 4; } else { diff --git a/tools/esp8266/hmRadio.h b/tools/esp8266/hmRadio.h index a251b7db..492b93d7 100644 --- a/tools/esp8266/hmRadio.h +++ b/tools/esp8266/hmRadio.h @@ -58,14 +58,14 @@ class HmRadio { mRxChIdx = 0; mRxLoopCnt = RX_LOOP_CNT; - //calcDtuCrc(); - pinCs = CS_PIN; pinCe = CE_PIN; pinIrq = IRQ_PIN; AmplifierPower = 1; mSendCnt = 0; + + mSerialDebug = false; } ~HmRadio() {} @@ -111,7 +111,7 @@ class HmRadio { if(!mBufCtrl->full()) { p = mBufCtrl->getFront(); memset(p->packet, 0xcc, MAX_RF_PAYLOAD_SIZE); - p->rxCh = mRxChIdx; + p->rxCh = mRxChLst[mRxChIdx]; len = mNrf24.getPayloadSize(); if(len > MAX_RF_PAYLOAD_SIZE) len = MAX_RF_PAYLOAD_SIZE; @@ -156,19 +156,19 @@ class HmRadio { sendPacket(invId, mTxBuf, 27, true); } - void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t cmd, bool calcCrc = true) { + void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool calcCrc = true) { memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); mTxBuf[0] = mid; // message id CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); CP_U32_BigEndian(&mTxBuf[5], (DTU_ID >> 8)); - mTxBuf[9] = cmd; + mTxBuf[9] = pid; if(calcCrc) { mTxBuf[10] = crc8(mTxBuf, 10); sendPacket(invId, mTxBuf, 11, false); } } - bool checkPaketCrc(uint8_t buf[], uint8_t *len, uint8_t *rptCnt, uint8_t rxCh) { + bool checkPaketCrc(uint8_t buf[], uint8_t *len, uint8_t rxCh) { *len = (buf[0] >> 2); if(*len > (MAX_RF_PAYLOAD_SIZE - 2)) *len = MAX_RF_PAYLOAD_SIZE - 2; @@ -179,17 +179,10 @@ class HmRadio { uint8_t crc = crc8(buf, *len-1); bool valid = (crc == buf[*len-1]); - if(valid) { - if(mLastCrc == crc) - *rptCnt = (++mRptCnt); - else { - mRptCnt = 0; - *rptCnt = 0; - mLastCrc = crc; - } - mRxStat[(buf[9] & 0x7F)-1]++; - mRxChStat[(buf[9] & 0x7F)-1][rxCh & 0x7]++; - } + //if(valid) { + //mRxStat[(buf[9] & 0x7F)-1]++; + //mRxChStat[(buf[9] & 0x7F)-1][rxCh & 0x7]++; + //} /*else { DPRINT("CRC wrong: "); DHEX(crc); @@ -215,7 +208,8 @@ class HmRadio { } void dumpBuf(const char *info, uint8_t buf[], uint8_t len) { - DPRINT(String(info)); + if(NULL != info) + DPRINT(String(info)); for(uint8_t i = 0; i < len; i++) { DHEX(buf[i]); DPRINT(" "); @@ -234,16 +228,22 @@ class HmRadio { uint8_t AmplifierPower; uint32_t mSendCnt; + bool mSerialDebug; + private: void sendPacket(uint64_t invId, uint8_t buf[], uint8_t len, bool clear=false) { //DPRINTLN("sent packet: #" + String(mSendCnt)); //dumpBuf("SEN ", buf, len); + if(mSerialDebug) { + DPRINT("Transmit " + String(len) + " | "); + dumpBuf(NULL, buf, len); + } DISABLE_IRQ; mNrf24.stopListening(); if(clear) { - uint8_t cnt = 4; + /*uint8_t cnt = 4; for(uint8_t i = 0; i < 4; i ++) { DPRINT(String(mRxStat[i]) + " ("); for(uint8_t j = 0; j < 4; j++) { @@ -258,7 +258,7 @@ class HmRadio { else DPRINTLN(" -> missing: " + String(cnt)); memset(mRxStat, 0, 4); - memset(mRxChStat, 0, 4*8); + memset(mRxChStat, 0, 4*8);*/ mRxLoopCnt = RX_LOOP_CNT; } @@ -295,34 +295,19 @@ class HmRadio { return mRxChLst[mRxChIdx]; } - /*void calcDtuCrc(void) { - uint64_t addr = DTU_RADIO_ID; - uint8_t tmp[5]; - for(int8_t i = 4; i >= 0; i--) { - tmp[i] = addr; - addr >>= 8; - } - mDtuIdCrc = crc16nrf24(tmp, BIT_CNT(5)); - }*/ - uint8_t mTxCh; uint8_t mTxChLst[1]; //uint8_t mTxChIdx; uint8_t mRxChLst[4]; uint8_t mRxChIdx; - uint8_t mRxStat[4]; - uint8_t mRxChStat[4][8]; + //uint8_t mRxStat[4]; + //uint8_t mRxChStat[4][8]; uint16_t mRxLoopCnt; - //uint16_t mDtuIdCrc; - uint16_t mLastCrc; - uint8_t mRptCnt; - RF24 mNrf24; BUFFER *mBufCtrl; uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; - }; #endif /*__RADIO_H__*/ diff --git a/tools/esp8266/hmSystem.h b/tools/esp8266/hmSystem.h index c418fb08..3bc202a3 100644 --- a/tools/esp8266/hmSystem.h +++ b/tools/esp8266/hmSystem.h @@ -27,7 +27,7 @@ class HmSystem { Radio.setup(&BufCtrl); } - INVERTERTYPE *addInverter(const char *name, uint64_t serial, uint8_t type) { + INVERTERTYPE *addInverter(const char *name, uint64_t serial) { if(MAX_INVERTER <= mNumInv) { DPRINT("max number of inverters reached!"); return NULL; @@ -35,7 +35,19 @@ class HmSystem { INVERTERTYPE *p = &mInverter[mNumInv]; p->id = mNumInv; p->serial.u64 = serial; - p->type = type; + DPRINT("SERIAL: " + String(p->serial.b[5], HEX)); + DPRINTLN(" " + String(p->serial.b[4], HEX)); + if(p->serial.b[5] == 0x11) { + switch(p->serial.b[4]) { + case 0x21: p->type = INV_TYPE_1CH; break; + case 0x41: p->type = INV_TYPE_2CH; break; + case 0x61: p->type = INV_TYPE_4CH; break; + default: DPRINTLN("unknown inverter type: 11" + String(p->serial.b[4], HEX)); break; + } + } + else + DPRINTLN("inverter type can't be detected!"); + p->init(); uint8_t len = (uint8_t)strlen(name); strncpy(p->name, name, (len > MAX_NAME_LENGTH) ? MAX_NAME_LENGTH : len); From 6871bf4247e283c163f220d973862198c670fb1a Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 17 May 2022 17:15:44 +0200 Subject: [PATCH 08/10] * multi inverter support * full re transmit included --- tools/esp8266/app.cpp | 195 ++++++++++++++++++++-------------------- tools/esp8266/app.h | 25 +++--- tools/esp8266/defines.h | 2 +- 3 files changed, 114 insertions(+), 108 deletions(-) diff --git a/tools/esp8266/app.cpp b/tools/esp8266/app.cpp index 472aeeb3..05e46268 100644 --- a/tools/esp8266/app.cpp +++ b/tools/esp8266/app.cpp @@ -23,12 +23,8 @@ app::app() : Main() { mSerialValues = true; mSerialDebug = false; - memset(mPacketIds, 0, sizeof(uint32_t)*DBG_CMD_LIST_LEN); - - memset(mPayload, 0, (MAX_PAYLOAD_ENTRIES * MAX_RF_PAYLOAD_SIZE)); - memset(mPayloadLen, 0, MAX_PAYLOAD_ENTRIES); - mPayloadComplete = true; - mMaxPackId = 0; + memset(mPayload, 0, (MAX_NUM_INVERTERS * sizeof(invPayload_t))); + mRxFailed = 0; mSys = new HmSystemType(); } @@ -139,7 +135,7 @@ void app::loop(void) { Main::loop(); if(checkTicker(&mRxTicker, 5)) { - bool rcvRdy = mSys->Radio.switchRxCh(); + bool rxRdy = mSys->Radio.switchRxCh(); if(!mSys->BufCtrl.empty()) { uint8_t len; @@ -153,34 +149,19 @@ void app::loop(void) { } if(0 != len) { - uint8_t *packetId = &p->packet[9]; - if((*packetId & 0x7F) < 5) { - memcpy(mPayload[(*packetId & 0x7F) - 1], &p->packet[10], len-11); - mPayloadLen[(*packetId & 0x7F) - 1] = len-11; - } - - if((*packetId & 0x80) == 0x80) { - if((*packetId & 0x7f) > mMaxPackId) - mMaxPackId = (*packetId & 0x7f); - } - - /*Inverter<> *iv = mSys->findInverter(&p->packet[1]); + Inverter<> *iv = mSys->findInverter(&p->packet[1]); if(NULL != iv) { - for(uint8_t i = 0; i < iv->listLen; i++) { - if(iv->assign[i].cmdId == *packetId) - iv->addValue(i, &p->packet[9]); + uint8_t *pid = &p->packet[9]; + if((*pid & 0x7F) < 5) { + memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], len-11); + mPayload[iv->id].len[(*pid & 0x7F) - 1] = len-11; } - iv->doCalculations(); - }*/ - if(*packetId == 0x01) mPacketIds[0]++; - else if(*packetId == 0x02) mPacketIds[1]++; - else if(*packetId == 0x03) mPacketIds[2]++; - else if(*packetId == 0x81) mPacketIds[3]++; - else if(*packetId == 0x82) mPacketIds[4]++; - else if(*packetId == 0x83) mPacketIds[5]++; - else if(*packetId == 0x84) mPacketIds[6]++; - else mPacketIds[7]++; + if((*pid & 0x80) == 0x80) { + if((*pid & 0x7f) > mPayload[iv->id].maxPackId) + mPayload[iv->id].maxPackId = (*pid & 0x7f); + } + } } } @@ -188,43 +169,8 @@ void app::loop(void) { } - // TODO: support more than one inverter! - if(rcvRdy && (!mPayloadComplete)) { - Inverter<> *iv = mSys->getInverterByPos(0); - if(!buildPayload()) { - if(mMaxPackId != 0) { - for(uint8_t i = 0; i < (mMaxPackId-1); i ++) { - // retransmit - if(mPayloadLen[i] == 0) { - if(mSerialDebug) - DPRINTLN("Error while retrieving data: Frame " + String(i+1) + " missing: Request Retransmit"); - mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x15, (0x81+i), true); - } - } - } - mSys->Radio.switchRxCh(200); - } - else { - mPayloadComplete = true; - uint8_t payload[256] = {0}; - uint8_t offs = 0; - for(uint8_t i = 0; i < (mMaxPackId); i ++) { - memcpy(&payload[offs], mPayload[i], (mPayloadLen[i])); - offs += (mPayloadLen[i]); - } - offs-=2; - if(mSerialDebug) { - DPRINT("Payload (" + String(offs) + "): "); - mSys->Radio.dumpBuf(NULL, payload, offs); - } - - if(NULL != iv) { - for(uint8_t i = 0; i < iv->listLen; i++) { - iv->addValue(i, payload); - } - iv->doCalculations(); - } - } + if(rxRdy) { + processPayload(true); } } @@ -274,24 +220,34 @@ void app::loop(void) { if(++mSendTicker >= mSendInterval) { mSendTicker = 0; - memset(mPayloadLen, 0, MAX_PAYLOAD_ENTRIES); - mMaxPackId = 0; - if(mSerialDebug) { - if(!mPayloadComplete) - DPRINTLN("no Payload received!"); - } - mPayloadComplete = false; - if(!mSys->BufCtrl.empty()) { if(mSerialDebug) DPRINTLN("recbuf not empty! #" + String(mSys->BufCtrl.getFill())); } - Inverter<> *inv; + Inverter<> *iv; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { - inv = mSys->getInverterByPos(i); - if(NULL != inv) { + iv = mSys->getInverterByPos(i); + if(NULL != iv) { + // reset payload data + memset(mPayload[iv->id].len, 0, MAX_PAYLOAD_ENTRIES); + mPayload[iv->id].maxPackId = 0; + if(mSerialDebug) { + if(!mPayload[iv->id].complete) + processPayload(false); + + if(!mPayload[iv->id].complete) { + DPRINT("Inverter #" + String(iv->id) + " "); + DPRINTLN("no Payload received!"); + mRxFailed++; + } + } + mPayload[iv->id].complete = false; + mPayload[iv->id].ts = mTimestamp; + yield(); - mSys->Radio.sendTimePacket(inv->radioId.u64, mTimestamp); + if(mSerialDebug) + DPRINTLN("Requesting Inverter SN " + String(iv->serial.u64, HEX)); + mSys->Radio.sendTimePacket(iv->radioId.u64, mPayload[iv->id].ts); mRxTicker = 0; } } @@ -307,21 +263,21 @@ void app::handleIntr(void) { //----------------------------------------------------------------------------- -bool app::buildPayload() { +bool app::buildPayload(uint8_t id) { //DPRINTLN("Payload"); uint16_t crc = 0xffff, crcRcv; - if(mMaxPackId > MAX_PAYLOAD_ENTRIES) - mMaxPackId = MAX_PAYLOAD_ENTRIES; + if(mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES) + mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; - for(uint8_t i = 0; i < mMaxPackId; i ++) { - if(mPayloadLen[i] > 0) { - if(i == (mMaxPackId-1)) { - crc = crc16(mPayload[i], mPayloadLen[i] - 2, crc); - crcRcv = (mPayload[i][mPayloadLen[i] - 2] << 8) - | (mPayload[i][mPayloadLen[i] - 1]); + for(uint8_t i = 0; i < mPayload[id].maxPackId; i ++) { + if(mPayload[id].len[i] > 0) { + if(i == (mPayload[id].maxPackId-1)) { + crc = crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc); + crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) + | (mPayload[id].data[i][mPayload[id].len[i] - 1]); } else - crc = crc16(mPayload[i], mPayloadLen[i], crc); + crc = crc16(mPayload[id].data[i], mPayload[id].len[i], crc); } } if(crc == crcRcv) @@ -330,6 +286,56 @@ bool app::buildPayload() { } +//----------------------------------------------------------------------------- +void app::processPayload(bool retransmit) { + for(uint8_t id = 0; id < mSys->getNumInverters(); id++) { + Inverter<> *iv = mSys->getInverterByPos(id); + if(NULL != iv) { + if(!mPayload[iv->id].complete) { + if(!buildPayload(iv->id)) { + if(retransmit) { + if(mPayload[iv->id].maxPackId != 0) { + for(uint8_t i = 0; i < (mPayload[iv->id].maxPackId-1); i ++) { + if(mPayload[iv->id].len[i] == 0) { + if(mSerialDebug) + DPRINTLN("Error while retrieving data: Frame " + String(i+1) + " missing: Request Retransmit"); + mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x15, (0x81+i), true); + } + } + } + else { + if(mSerialDebug) + DPRINTLN("Error while retrieving data: last frame missing: Request Retransmit"); + mSys->Radio.sendTimePacket(iv->radioId.u64, mPayload[iv->id].ts); + } + mSys->Radio.switchRxCh(100); + } + } + else { + mPayload[iv->id].complete = true; + uint8_t payload[128] = {0}; + uint8_t offs = 0; + for(uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i ++) { + memcpy(&payload[offs], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); + offs += (mPayload[iv->id].len[i]); + } + offs-=2; + if(mSerialDebug) { + DPRINT("Payload (" + String(offs) + "): "); + mSys->Radio.dumpBuf(NULL, payload, offs); + } + + for(uint8_t i = 0; i < iv->listLen; i++) { + iv->addValue(i, payload); + } + iv->doCalculations(); + } + } + } + } +} + + //----------------------------------------------------------------------------- void app::showIndex(void) { String html = FPSTR(index_html); @@ -473,12 +479,7 @@ void app::showErase() { //----------------------------------------------------------------------------- void app::showStatistics(void) { - String content = "Packets:\n"; - for(uint8_t i = 0; i < DBG_CMD_LIST_LEN; i ++) { - content += String("0x") + String(dbgCmds[i], HEX) + String(": ") + String(mPacketIds[i]) + String("\n"); - } - content += String("other: ") + String(mPacketIds[DBG_CMD_LIST_LEN]) + String("\n\n"); - + String content = "Failed Payload: " + String(mRxFailed) + "\n"; content += "Send Cnt: " + String(mSys->Radio.mSendCnt) + String("\n\n"); if(!mSys->Radio.isChipConnected()) diff --git a/tools/esp8266/app.h b/tools/esp8266/app.h index e1047a80..d7c5736f 100644 --- a/tools/esp8266/app.h +++ b/tools/esp8266/app.h @@ -19,12 +19,20 @@ typedef HmSystem HmSyste const char* const wemosPins[] = {"D3 (GPIO0)", "TX (GPIO1)", "D4 (GPIO2)", "RX (GPIO3)", "D2 (GPIO4)", "D1 (GPIO5)", "GPIO6", "GPIO7", "GPIO8", "GPIO9", "GPIO10", "GPIO11", "D6 (GPIO12)", "D7 (GPIO13)", - "D5 (GPIO14)", "D8 (GPIO15)", "D0 (GPIO16)"}; + "D5 (GPIO14)", "D8 (GPIO15)", "D0 (GPIO16 - no IRQ!)"}; const char* const pinNames[] = {"CS", "CE", "IRQ"}; const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq"}; -const uint8_t dbgCmds[] = {0x01, 0x02, 0x03, 0x81, 0x82, 0x83, 0x84}; -#define DBG_CMD_LIST_LEN 7 + +typedef struct { + uint8_t invId; + uint32_t ts; + uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; + uint8_t len[MAX_PAYLOAD_ENTRIES]; + bool complete; + uint8_t maxPackId; +} invPayload_t; + class app : public Main { public: @@ -40,7 +48,8 @@ class app : public Main { } private: - bool buildPayload(); + bool buildPayload(uint8_t id); + void processPayload(bool retransmit); void showIndex(void); void showSetup(void); @@ -75,12 +84,8 @@ class app : public Main { uint16_t mSendTicker; uint16_t mSendInterval; - uint32_t mPacketIds[DBG_CMD_LIST_LEN+1]; - uint32_t mRecCnt; - uint8_t mPayload[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; - uint8_t mPayloadLen[MAX_PAYLOAD_ENTRIES]; - bool mPayloadComplete; - uint8_t mMaxPackId; + invPayload_t mPayload[MAX_NUM_INVERTERS]; + uint32_t mRxFailed; // timer uint32_t mTicker; diff --git a/tools/esp8266/defines.h b/tools/esp8266/defines.h index bcad6275..de135cb2 100644 --- a/tools/esp8266/defines.h +++ b/tools/esp8266/defines.h @@ -16,7 +16,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 4 -#define VERSION_PATCH 0 +#define VERSION_PATCH 1 //------------------------------------- From 5acce759e014de318ce0c59eeae4fc047d84d60e Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 17 May 2022 20:05:31 +0200 Subject: [PATCH 09/10] * fix #39 Assignment 2-Channel inverters (HM-600, HM-700, HM-800) --- tools/esp8266/defines.h | 2 +- tools/esp8266/hmDefines.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/esp8266/defines.h b/tools/esp8266/defines.h index de135cb2..fee7211b 100644 --- a/tools/esp8266/defines.h +++ b/tools/esp8266/defines.h @@ -16,7 +16,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 4 -#define VERSION_PATCH 1 +#define VERSION_PATCH 2 //------------------------------------- diff --git a/tools/esp8266/hmDefines.h b/tools/esp8266/hmDefines.h index 1f8c9389..15b55ea1 100644 --- a/tools/esp8266/hmDefines.h +++ b/tools/esp8266/hmDefines.h @@ -76,13 +76,13 @@ const byteAssign_t hm2chAssignment[] = { { FLD_IDC, UNIT_A, CH1, 4, 2, 100 }, { FLD_PDC, UNIT_W, CH1, 6, 2, 10 }, { FLD_YD, UNIT_WH, CH1, 22, 2, 1 }, - { FLD_YD, UNIT_KWH, CH1, 14, 4, 1000 }, + { FLD_YT, UNIT_KWH, CH1, 14, 4, 1000 }, { FLD_UDC, UNIT_V, CH2, 8, 2, 10 }, { FLD_IDC, UNIT_A, CH2, 10, 2, 100 }, { FLD_PDC, UNIT_W, CH2, 12, 2, 10 }, { FLD_YD, UNIT_WH, CH2, 18, 2, 1 }, - { FLD_YD, UNIT_KWH, CH2, 24, 4, 1000 }, + { FLD_YT, UNIT_KWH, CH2, 24, 4, 1000 }, { FLD_UAC, UNIT_V, CH0, 26, 2, 10 }, { FLD_IAC, UNIT_A, CH0, 34, 2, 10 }, From a35cb414a822126fb66753895bcca68133e941ca Mon Sep 17 00:00:00 2001 From: lumapu Date: Tue, 17 May 2022 21:47:41 +0200 Subject: [PATCH 10/10] * fixed #41 HM800 Yield total and Yield day were mixed around. Found issue while comparing to Python version * fixed #43 HM350 channel 2 is displayed in Live-View * added #42 YieldTotal and YieldTotal Day for HM600 - HM800 inverters --- tools/esp8266/app.cpp | 4 ++-- tools/esp8266/defines.h | 2 +- tools/esp8266/hmDefines.h | 8 +++++--- tools/esp8266/html/h/style_css.h | 2 +- tools/esp8266/html/style.css | 15 +++++++++++++++ 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/tools/esp8266/app.cpp b/tools/esp8266/app.cpp index 05e46268..8f18b1df 100644 --- a/tools/esp8266/app.cpp +++ b/tools/esp8266/app.cpp @@ -518,8 +518,8 @@ void app::showLiveData(void) { #ifdef LIVEDATA_VISUALIZED uint8_t modNum, pos; switch(iv->type) { - default: modNum = 1; break; - case INV_TYPE_1CH: + default: + case INV_TYPE_1CH: modNum = 1; break; case INV_TYPE_2CH: modNum = 2; break; case INV_TYPE_4CH: modNum = 4; break; } diff --git a/tools/esp8266/defines.h b/tools/esp8266/defines.h index fee7211b..42ffc784 100644 --- a/tools/esp8266/defines.h +++ b/tools/esp8266/defines.h @@ -16,7 +16,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 4 -#define VERSION_PATCH 2 +#define VERSION_PATCH 3 //------------------------------------- diff --git a/tools/esp8266/hmDefines.h b/tools/esp8266/hmDefines.h index 15b55ea1..4cba1791 100644 --- a/tools/esp8266/hmDefines.h +++ b/tools/esp8266/hmDefines.h @@ -81,14 +81,16 @@ const byteAssign_t hm2chAssignment[] = { { FLD_UDC, UNIT_V, CH2, 8, 2, 10 }, { FLD_IDC, UNIT_A, CH2, 10, 2, 100 }, { FLD_PDC, UNIT_W, CH2, 12, 2, 10 }, - { FLD_YD, UNIT_WH, CH2, 18, 2, 1 }, - { FLD_YT, UNIT_KWH, CH2, 24, 4, 1000 }, + { FLD_YD, UNIT_WH, CH2, 24, 2, 1 }, + { FLD_YT, UNIT_KWH, CH2, 18, 4, 1000 }, { FLD_UAC, UNIT_V, CH0, 26, 2, 10 }, { FLD_IAC, UNIT_A, CH0, 34, 2, 10 }, { FLD_PAC, UNIT_W, CH0, 30, 2, 10 }, { FLD_F, UNIT_HZ, CH0, 28, 2, 100 }, - { FLD_T, UNIT_C, CH0, 38, 2, 10 } + { FLD_T, UNIT_C, CH0, 38, 2, 10 }, + { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, + { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC } }; #define HM2CH_LIST_LEN (sizeof(hm2chAssignment) / sizeof(byteAssign_t)) diff --git a/tools/esp8266/html/h/style_css.h b/tools/esp8266/html/h/style_css.h index b64ceeb6..082d4769 100644 --- a/tools/esp8266/html/h/style_css.h +++ b/tools/esp8266/html/h/style_css.h @@ -1,4 +1,4 @@ #ifndef __STYLE_CSS_H__ #define __STYLE_CSS_H__ -const char style_css[] PROGMEM = "h1 {margin:0;padding:20pt;font-size:22pt;color:#fff;background-color:#006ec0;display:block;text-transform:uppercase;}html, body {font-family:Arial;margin:0;padding:0;}p {text-align:justify;font-size:13pt;}.des {margin-top:35px;font-size:13pt;color:#006ec0;}.subdes {font-size:12pt;color:#006ec0;margin-left:7px;}a:link, a:visited {text-decoration:none;font-size:13pt;color:#006ec0;}a:hover, a:focus {color:#f00;}a.erase {background-color:#006ec0;color:#fff;padding:7px;display:inline-block;margin-top:30px;float:right;}#content {padding:15px 15px 60px 15px;}#footer {position:fixed;bottom:0px;height:45px;background-color:#006ec0;width:100%;border-top:5px solid #fff;}#footer p, #footer a {color:#fff;padding:0 7px 0 7px;font-size:10pt !important;}div.content {background-color:#fff;padding-bottom:65px;overflow:auto;}input, select {padding:7px;font-size:13pt;}input.text, select {width:70%;box-sizing:border-box;margin-bottom:10px;border:1px solid #ccc;}input.btn {background-color:#006ec0;color:#fff;border:0px;float:right;margin:10px 0 30px;text-transform:uppercase;}input.cb {margin-bottom:20px;}label {width:20%;display:inline-block;font-size:12pt;padding-right:10px;margin-left:10px;}.left {float:left;}.right {float:right;}div.ch-iv {width:100%;background-color:#32b004;display:inline-block;margin-bottom:20px;padding-bottom:20px;overflow:auto;}div.ch {width:250px;min-height:420px;background-color:#006ec0;display:inline-block;margin-right:20px;margin-bottom:20px;overflow:auto;padding-bottom:20px;}div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head {color:#fff;display:block;width:100%;text-align:center;}.subgrp {float:left;width:250px;}div.ch .unit, div.ch-iv .unit {font-size:19px;margin-left:10px;}div.ch .value, div.ch-iv .value {margin-top:20px;font-size:30px;}div.ch .info, div.ch-iv .info {margin-top:3px;font-size:10px;}div.ch .head {background-color:#003c80;padding:10px 0 10px 0;}div.ch-iv .head {background-color:#1c6800;padding:10px 0 10px 0;}div.iv {max-width:1060px;}div.ch:last-child {margin-right:0px !important;}#note {margin:50px 10px 10px 10px;padding-top:10px;width:100%;border-top:1px solid #bbb;}"; +const char style_css[] PROGMEM = "h1 {margin:0;padding:20pt;font-size:22pt;color:#fff;background-color:#006ec0;display:block;text-transform:uppercase;}html, body {font-family:Arial;margin:0;padding:0;}p {text-align:justify;font-size:13pt;}.des {margin-top:35px;font-size:13pt;color:#006ec0;}.subdes {font-size:12pt;color:#006ec0;margin-left:7px;}a:link, a:visited {text-decoration:none;font-size:13pt;color:#006ec0;}a:hover, a:focus {color:#f00;}a.erase {background-color:#006ec0;color:#fff;padding:7px;display:inline-block;margin-top:30px;float:right;}#content {padding:15px 15px 60px 15px;}#footer {position:fixed;bottom:0px;height:45px;background-color:#006ec0;width:100%;border-top:5px solid #fff;}#footer p, #footer a {color:#fff;padding:0 7px 0 7px;font-size:10pt !important;}div.content {background-color:#fff;padding-bottom:65px;overflow:auto;}input, select {padding:7px;font-size:13pt;}input.text, select {width:70%;box-sizing:border-box;margin-bottom:10px;border:1px solid #ccc;}input.btn {background-color:#006ec0;color:#fff;border:0px;float:right;margin:10px 0 30px;text-transform:uppercase;}input.cb {margin-bottom:20px;}label {width:20%;display:inline-block;font-size:12pt;padding-right:10px;margin-left:10px;}.left {float:left;}.right {float:right;}div.ch-iv {width:100%;background-color:#32b004;display:inline-block;margin-bottom:20px;padding-bottom:20px;overflow:auto;}div.ch {width:250px;min-height:420px;background-color:#006ec0;display:inline-block;margin-right:20px;margin-bottom:20px;overflow:auto;padding-bottom:20px;}div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head {color:#fff;display:block;width:100%;text-align:center;}.subgrp {float:left;width:250px;}div.ch .unit, div.ch-iv .unit {font-size:19px;margin-left:10px;}div.ch .value, div.ch-iv .value {margin-top:20px;font-size:30px;}div.ch .info, div.ch-iv .info {margin-top:3px;font-size:10px;}div.ch .head {background-color:#003c80;padding:10px 0 10px 0;}div.ch-iv .head {background-color:#1c6800;padding:10px 0 10px 0;}div.iv {max-width:1060px;}div.ch:last-child {margin-right:0px !important;}#note {margin:50px 10px 10px 10px;padding-top:10px;width:100%;border-top:1px solid #bbb;}@media(max-width:500px) {div.ch .unit, div.ch-iv .unit {font-size:18px;}div.ch {width:170px;min-height:100px;}.subgrp {width:180px;}}"; #endif /*__STYLE_CSS_H__*/ diff --git a/tools/esp8266/html/style.css b/tools/esp8266/html/style.css index 89d8db7f..e245c340 100644 --- a/tools/esp8266/html/style.css +++ b/tools/esp8266/html/style.css @@ -188,3 +188,18 @@ div.ch:last-child { width: 100%; border-top: 1px solid #bbb; } + +@media(max-width: 500px) { + div.ch .unit, div.ch-iv .unit { + font-size: 18px; + } + + div.ch { + width: 170px; + min-height: 100px; + } + + .subgrp { + width: 180px; + } +}