From 93ae88fca20b4239625f3ca7f66926cfca2cd449 Mon Sep 17 00:00:00 2001 From: PaeserBastelstube <122045840+PaeserBastelstube@users.noreply.github.com> Date: Thu, 5 Jan 2023 18:50:44 +0100 Subject: [PATCH 01/23] change AC_CURRENT divider to 100 In line 514, ac_current dividor must be 100 not 10 There is a mismatch in AC output: power[Watt] = voltage[Volt] * current[Ampere] --- 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 0935110e..379a472a 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -511,7 +511,7 @@ class Hm600Decode0B(StatusResponse): @property def ac_current_0(self): """ Phase 1 ampere """ - return self.unpack('>H', 34)[0]/10 + return self.unpack('>H', 34)[0]/100 @property def ac_power_0(self): """ Phase 1 watts """ From 7dedcc2c8af677bcce997e0433d34ee35cf30fe7 Mon Sep 17 00:00:00 2001 From: PaeserBastelstube <122045840+PaeserBastelstube@users.noreply.github.com> Date: Sat, 7 Jan 2023 13:25:19 +0100 Subject: [PATCH 02/23] Add mqtt global head (print timestamp) and add DC-yield-daily MQTT record must have a timestamp for visualising last data received. DC-yield-daily is availible in "data" variable, but not printed in mqtt record. MQTT topics between ESP and RPI shoud be equel, so I sugest to change topic for YieldTotal --- tools/rpi/hoymiles/__main__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 8fc1b727..8a163aa5 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -209,6 +209,10 @@ def mqtt_send_status(broker, inverter_ser, data, topic=None): if not topic: topic = f'hoymiles/{inverter_ser}' + # Global Head + if data['time'] is not None: + broker.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S")) + # AC Data phase_id = 0 for phase in data['phases']: @@ -220,10 +224,11 @@ def mqtt_send_status(broker, inverter_ser, data, topic=None): # DC Data string_id = 0 for string in data['strings']: - broker.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000) - broker.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) broker.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) broker.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) + broker.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) + broker.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily']) + broker.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000) string_id = string_id + 1 # Global if data['powerfactor'] is not None: From 1a435b4e7bab1e2777b6ae8fab3796d420723ab6 Mon Sep 17 00:00:00 2001 From: Sarge Date: Sat, 7 Jan 2023 21:34:53 +0100 Subject: [PATCH 03/23] MQTT topic input: allow more special chars (#$%&), removed needles escapes / minus at end as required by regex --- src/web/html/setup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 8377cede..c60020e1 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -147,7 +147,7 @@ - +

Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)

From 2c7e60352d3ecf6d893e7fe6ede53cc5d7dd23c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Sun, 8 Jan 2023 18:17:55 +0100 Subject: [PATCH 04/23] RPI: reactive power (Blindleistung) missing we are missing "weactive power" (Blindleistung) in our output on RPi. On ESP it is working allready. --- tools/rpi/hoymiles/decoders/__init__.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index 379a472a..f67cdfda 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -91,7 +91,7 @@ class Response: class StatusResponse(Response): """Inverter StatusResponse object""" - e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor'] + e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor', 'reactive_power'] temperature = None frequency = None powerfactor = None @@ -438,6 +438,10 @@ class Hm300Decode0B(StatusResponse): """ Grid frequency in Hertz """ return self.unpack('>H', 16)[0]/100 @property + def ac_reactive_power_0(self): + """ reactive power """ + return self.unpack('>H', 20)[0]/10 + @property def temperature(self): """ Inverter temperature in °C """ return self.unpack('>h', 26)[0]/10 @@ -521,6 +525,10 @@ class Hm600Decode0B(StatusResponse): """ Grid frequency in Hertz """ return self.unpack('>H', 28)[0]/100 @property + def ac_reactive_power_0(self): + """ reactive power """ + return self.unpack('>H', 32)[0]/10 + @property def powerfactor(self): """ Powerfactor """ return self.unpack('>H', 36)[0]/1000 @@ -654,6 +662,10 @@ class Hm1200Decode0B(StatusResponse): """ Grid frequency in Hertz """ return self.unpack('>H', 48)[0]/100 @property + def ac_reactive_power_0(self): + """ reactive power """ + return self.unpack('>H', 52)[0]/10 + @property def powerfactor(self): """ Powerfactor """ return self.unpack('>H', 56)[0]/1000 From 94cd4eb5bda7d9691552b85f2aebae8de8054193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Sun, 8 Jan 2023 18:30:22 +0100 Subject: [PATCH 05/23] RPi: miss reactice power - add MQTT output [Q_VA] Add MQTT Output [Q_VA] for reactive Power --- tools/rpi/hoymiles/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 8a163aa5..2fd8f991 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -219,6 +219,7 @@ def mqtt_send_status(broker, inverter_ser, data, topic=None): broker.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) broker.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) broker.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) + broker.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power']) phase_id = phase_id + 1 # DC Data From e3b0f55467b29c9c28602d555cccf7bdaf062212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Mon, 9 Jan 2023 20:58:55 +0100 Subject: [PATCH 06/23] Update output for VZ and Influx In last days, we update MQTT output, now we sync this updates for VZ and Influx. - reactive_power - Q_AC - YieldDay and YieldTotal --- tools/rpi/hoymiles/outputs.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index a365033a..e2f05e2b 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -101,18 +101,20 @@ class InfluxOutputPlugin(OutputPluginFactory): # 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}') + 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}') 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}') + 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}') string_id = string_id + 1 # Global if data['event_count'] is not None: @@ -241,19 +243,20 @@ class VzInverterOutput: # AC Data phase_id = 0 for phase in data['phases']: - self.try_publish(ts, f'ac_power{phase_id}', phase['power']) self.try_publish(ts, f'ac_voltage{phase_id}', phase['voltage']) self.try_publish(ts, f'ac_current{phase_id}', phase['current']) + self.try_publish(ts, f'ac_power{phase_id}', phase['power']) + self.try_publish(ts, f'ac_Q{phase_id}', phase['reactive_power']) phase_id = phase_id + 1 # DC Data string_id = 0 for string in data['strings']: - self.try_publish(ts, f'dc_power{string_id}', string['power']) self.try_publish(ts, f'dc_voltage{string_id}', string['voltage']) self.try_publish(ts, f'dc_current{string_id}', string['current']) - self.try_publish(ts, f'dc_total{string_id}', string['energy_total']) - self.try_publish(ts, f'dc_daily{string_id}', string['energy_daily']) + 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']) string_id = string_id + 1 # Global if data['powerfactor'] is not None: From 4df6eab987cf8848ab42f8435212dc9ee737ca6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:06:52 +0100 Subject: [PATCH 07/23] RPI: avoid error if mqtt is not defined If MQTT is not defined in ahoy.yml, ahoy send an AttributeError --- tools/rpi/hoymiles/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 2fd8f991..e3000806 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -345,8 +345,8 @@ if __name__ == '__main__': if global_config.verbose: hoymiles.HOYMILES_DEBUG_LOGGING=True - mqtt_config = ahoy_config.get('mqtt', []) - if not mqtt_config.get('disabled', False): + mqtt_config = ahoy_config.get('mqtt', {}) + if mqtt_config and not mqtt_config.get('disabled', False): mqtt_client = paho.mqtt.client.Client() if mqtt_config.get('useTLS',False): From 77087f1c377610023aeace644a4a22325a0c30d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Wed, 11 Jan 2023 14:42:30 +0100 Subject: [PATCH 08/23] RPI simpify and complete debug message Debug message in function "poll_inverter" is not maintainable. It is very easy to print a dict with all important data instead. --- tools/rpi/hoymiles/__main__.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index e3000806..842cf7fa 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -168,18 +168,7 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): data = result.__dict__() if hoymiles.HOYMILES_DEBUG_LOGGING: - dbg = f'{c_datetime} Decoded: temp={data["temperature"]}, total={data["energy_total"]/1000:.3f}' - if data['powerfactor'] is not None: - dbg += f', pf={data["powerfactor"]}' - phase_id = 0 - for phase in data['phases']: - dbg += f' phase{phase_id}=voltage:{phase["voltage"]}, current:{phase["current"]}, power:{phase["power"]}, frequency:{data["frequency"]}' - phase_id = phase_id + 1 - string_id = 0 - for string in data['strings']: - dbg += f' string{string_id}=voltage:{string["voltage"]}, current:{string["current"]}, power:{string["power"]}, total:{string["energy_total"]/1000}, daily:{string["energy_daily"]}' - string_id = string_id + 1 - logging.debug(dbg) + logging.debug(f'{c_datetime} Decoded: {result.__dict__()}') if 'event_count' in data: if event_message_index[inv_str] < data['event_count']: From 7fad17aa3e5dad1626e3fabe26834ed8d6c54699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Wed, 11 Jan 2023 15:01:54 +0100 Subject: [PATCH 09/23] RPI extend debug meassage for better understanding The response of HardwareInfoResponse contains 16 byte, but only 8 bytes are known. Analyse result of the last 2 bytes: This bytes contain the CRC-modbus. To observe the double-byte (6, 10, 12), this extended log message could help. --- tools/rpi/hoymiles/decoders/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index f67cdfda..b9ebc45e 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -333,7 +333,11 @@ class HardwareInfoResponse(UnknownResponse): { FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 }, { FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 }, { FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 }, - { FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 } + { FLD_unknown, UNIT_NONE, CH0, 6, 2, 1 }, + { FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 }, + { FLD_unknown, UNIT_NONE, CH0, 10, 2, 1 }, + { FLD_unknown, UNIT_NONE, CH0, 12, 2, 1 }, + { FLD_CRC-M, UNIT_NONE, CH0, 14, 2, 1 } }; self.response = bytes('\x27\x1a\x07\xe5\x04\x4d\x03\x4a\x00\x68\x00\x00\x00\x00\xe6\xfb', 'latin1') """ @@ -345,6 +349,8 @@ class HardwareInfoResponse(UnknownResponse): fw_build_mm = int(fw_build_mmdd / 100) fw_build_dd = int(fw_build_mmdd % 100) logging.debug(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} build at {fw_build_dd}/{fw_build_mm}/{fw_build_yyyy}, HW revision {hw_id}') + responce_info = self.response + logging.debug(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}') class DebugDecodeAny(UnknownResponse): """Default decoder""" From dcfd966fe400d44bf86cc474a1b1a31c1c3dda0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Wed, 11 Jan 2023 15:13:00 +0100 Subject: [PATCH 10/23] RPI move function MQTT-Output as class method RPI move function MQTT-Output as class method to outputs.py --- tools/rpi/hoymiles/__main__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index e3000806..45364a4d 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -187,14 +187,16 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.AlarmData, alarm_id=event_message_index[inv_str])) if mqtt_client: - mqtt_send_status(mqtt_client, inverter_ser, data, - topic=inverter.get('mqtt', {}).get('topic', None)) + # mqtt_send_status(mqtt_client, inverter_ser, data, topic=inverter.get('mqtt', {}).get('topic', None)) + mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None)) + if influx_client: influx_client.store_status(result) if volkszaehler_client: volkszaehler_client.store_status(result) +""" def mqtt_send_status(broker, inverter_ser, data, topic=None): """ Publish StatusResponse object @@ -238,6 +240,7 @@ def mqtt_send_status(broker, inverter_ser, data, topic=None): broker.publish(f'{topic}/temperature', data['temperature']) if data['energy_total'] is not None: broker.publish(f'{topic}/total', data['energy_total']/1000) +""" def mqtt_on_command(client, userdata, message): """ From bcd279ba039ae6b60f19de4cc2ec8f9790f87b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Wed, 11 Jan 2023 20:27:28 +0100 Subject: [PATCH 11/23] Update class MqttOutputPlugin movin function from main to class-method --- tools/rpi/hoymiles/outputs.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index e2f05e2b..4913f145 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -137,7 +137,7 @@ class MqttOutputPlugin(OutputPluginFactory): """ Mqtt output plugin """ client = None - def __init__(self, *args, **params): + def __init__(self, config, **params): """ Initialize MqttOutputPlugin @@ -158,11 +158,14 @@ class MqttOutputPlugin(OutputPluginFactory): :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) :type topic: str """ - super().__init__(*args, **params) + super().__init__(**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)) + if config.get('useTLS',False): + mqtt_client.tls_set() + mqtt_client.tls_insecure_set(config.get('insecureTLS',False)) + mqtt_client.username_pw_set(config.get('user', None), config.get('password', None)) + mqtt_client.connect(config.get('host', '127.0.0.1'), config.get('port', 1883)) mqtt_client.loop_start() self.client = mqtt_client @@ -182,8 +185,11 @@ class MqttOutputPlugin(OutputPluginFactory): raise ValueError('Data needs to be instance of StatusResponse') data = response.__dict__() + topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}' - topic = params.get('topic', f'hoymiles/{data["inverter_ser"]}') + # Global Head + if data['time'] is not None: + self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S")) # AC Data phase_id = 0 @@ -191,16 +197,18 @@ class MqttOutputPlugin(OutputPluginFactory): 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}/Q_AC', phase['reactive_power']) phase_id = phase_id + 1 # DC Data string_id = 0 for string in data['strings']: - 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']) + 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) string_id = string_id + 1 + # Global if data['powerfactor'] is not None: self.client.publish(f'{topic}/pf', data['powerfactor']) From f0894ca9511257bcfaabbbad233a34e0b15437a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Fri, 13 Jan 2023 12:36:54 +0100 Subject: [PATCH 12/23] RPi: correct error while calling MqttOutputPlugin In last patch I forgot the correct call to MqttOutputPlugin Now, it testet --- tools/rpi/hoymiles/__main__.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 32385d11..ac2181ad 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -185,7 +185,6 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): if volkszaehler_client: volkszaehler_client.store_status(result) -""" def mqtt_send_status(broker, inverter_ser, data, topic=None): """ Publish StatusResponse object @@ -229,7 +228,6 @@ def mqtt_send_status(broker, inverter_ser, data, topic=None): broker.publish(f'{topic}/temperature', data['temperature']) if data['energy_total'] is not None: broker.publish(f'{topic}/total', data['energy_total']/1000) -""" def mqtt_on_command(client, userdata, message): """ @@ -326,8 +324,6 @@ if __name__ == '__main__': for radio_config in ahoy_config.get('nrf', [{}]): hmradio = hoymiles.HoymilesNRF(**radio_config) - mqtt_client = None - event_message_index = {} command_queue = {} mqtt_command_topic_subs = [] @@ -337,18 +333,11 @@ if __name__ == '__main__': if global_config.verbose: hoymiles.HOYMILES_DEBUG_LOGGING=True + mqtt_client = None mqtt_config = ahoy_config.get('mqtt', {}) if mqtt_config and not mqtt_config.get('disabled', False): - mqtt_client = paho.mqtt.client.Client() - - if mqtt_config.get('useTLS',False): - mqtt_client.tls_set() - mqtt_client.tls_insecure_set(mqtt_config.get('insecureTLS',False)) - - mqtt_client.username_pw_set(mqtt_config.get('user', None), mqtt_config.get('password', None)) - mqtt_client.connect(mqtt_config.get('host', '127.0.0.1'), mqtt_config.get('port', 1883)) - mqtt_client.loop_start() - mqtt_client.on_message = mqtt_on_command + from .outputs import MqttOutputPlugin + mqtt_client = MqttOutputPlugin(mqtt_config) influx_client = None influx_config = ahoy_config.get('influxdb', {}) From 787da07457d18d050c4e3aab0fc75b136e6213fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Fri, 13 Jan 2023 13:05:59 +0100 Subject: [PATCH 13/23] RPi: submit attributes to decoder To import some attributes to data-dict-variable, this attributes must be submit to decoder classes --- tools/rpi/hoymiles/__main__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 32385d11..5eb21388 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -118,6 +118,8 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): :type retries: int """ inverter_ser = inverter.get('serial') + inverter_name = inverter.get('name') + inverter_strings = inverter.get('strings') # Queue at least status data request inv_str = str(inverter_ser) @@ -161,7 +163,10 @@ def poll_inverter(inverter, dtu_ser, do_init, retries): logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response)) decoder = hoymiles.ResponseDecoder(response, request=com.request, - inverter_ser=inverter_ser + inverter_ser=inverter_ser, + inverter_name=inverter_name, + dtu_ser=dtu_ser, + strings=inverter_strings ) result = decoder.decode() if isinstance(result, hoymiles.decoders.StatusResponse): From 5db34cd5f204de60cbb684d8a58dd6229fff2995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Fri, 13 Jan 2023 13:12:43 +0100 Subject: [PATCH 14/23] RPi: support to transmit some attributes To import some attribute to data-dict-variable, we need to transport this attributes to ResponseDecoder-Class --- tools/rpi/hoymiles/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index c02a0c71..169ad732 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -144,6 +144,9 @@ class ResponseDecoder(ResponseDecoderFactory): def __init__(self, response, **params): """Initialize ResponseDecoder""" ResponseDecoderFactory.__init__(self, response, **params) + self.inv_name=params.get('inverter_name', None) + self.dtu_ser=params.get('dtu_ser', None) + self.strings=params.get('strings', None) def decode(self): """ @@ -164,7 +167,10 @@ class ResponseDecoder(ResponseDecoderFactory): return device(self.response, time_rx=self.time_rx, - inverter_ser=self.inverter_ser + inverter_ser=self.inverter_ser, + inverter_name=self.inv_name, + dtu_ser=self.dtu_ser, + strings=self.strings ) class InverterPacketFragment: From 2eef3b5bb4dd9557b69d3d531668878aef1c231f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Fri, 13 Jan 2023 13:14:55 +0100 Subject: [PATCH 15/23] Create ahoy.yml privat ahoy-yml --- tools/rpi/ahoy.yml | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tools/rpi/ahoy.yml diff --git a/tools/rpi/ahoy.yml b/tools/rpi/ahoy.yml new file mode 100644 index 00000000..036ed193 --- /dev/null +++ b/tools/rpi/ahoy.yml @@ -0,0 +1,84 @@ +--- + +ahoy: + interval: 5 + + logging: + filename: 'RPI-AHOY-DTU.log' + # DEBUG, INFO, WARNING, ERROR, FATAL + level: 'DEBUG' + + sunset: + disabled: false + latitude: 52.485333 + longitude: 10.315783 + altitude: 55 + + # List of available NRF24 transceivers + nrf: + - ce_pin: 22 + cs_pin: 0 + txpower: 'low' # default txpower (min,low,high,max) + + mqtt: + disabled: false + host: 192.168.254.36 + port: 1883 + user: 'Raspberry' + password: 'rpi' + useTLS: False + insecureTLS: False #set True for e.g. self signed certificates. + + # Influx2 output + influxdb: + disabled: true + url: 'http://influxserver.local:8086' + org: 'myorg' + token: '' + bucket: 'telegraf/autogen' + measurement: 'hoymiles' + + volkszaehler: + disabled: true + inverters: + - serial: 114172220003 + url: 'http://localhost/middleware/' + channels: + - type: 'temperature' + uid: 'ad578a40-1d97-11ed-8e8b-fda01a416575' + - type: 'frequency' + uid: '' + - type: 'ac_power0' + uid: '7ca5ac50-1e41-11ed-927f-610c4cb2c69e' + - type: 'ac_voltage0' + uid: '9a38e2e0-1d94-11ed-b539-25f8607ac030' + - type: 'ac_current0' + uid: 'a9a4daf0-1e41-11ed-b68c-eb73eef3d21d' + - type: 'dc_power0' + uid: '38eb3ca0-1e53-11ed-b830-792e70a592fa' + - type: 'dc_voltage0' + uid: '' + - type: 'dc_current0' + uid: '' + - type: 'dc_power1' + uid: '51c0e9d0-1e53-11ed-b574-8bc81547eb8f' + - type: 'dc_voltage1' + uid: '' + - type: 'dc_current1' + uid: '' + + dtu: + serial: 99978563001 + + inverters: + - name: 'Appelweg_PV' # 2ch inverter 1141-Series + serial: 114181807700 # ser-nr starts with 1141... + txpower: 'low' # txpower per inverter (min,low,high,max) + mqtt: + send_raw_enabled: false # allow inject debug data via mqtt + topic: 'Appelweg_PV/114181807700' # defaults to 'hoymiles/{serial}' + strings: # list all available strings + - s_name: 'String 1 left' # String 1 name + s_maxpower: 405 # String 1 max power in Wp + - s_name: 'String 2 right' # String 2 name + s_maxpower: 405 # String 2 max power in Wp From 899dbaa346d008176c16afdbaac824095a206794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Fri, 13 Jan 2023 13:15:28 +0100 Subject: [PATCH 16/23] Create ahoy.service --- tools/rpi/ahoy.service | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tools/rpi/ahoy.service diff --git a/tools/rpi/ahoy.service b/tools/rpi/ahoy.service new file mode 100644 index 00000000..68b0a121 --- /dev/null +++ b/tools/rpi/ahoy.service @@ -0,0 +1,34 @@ +###################################################################### +# systemd.service configuration for ahoy (lumapu) +# users can modify the lines: +# - Description +# - ExecStart (for example: name of config file) +# - WorkingDirectory +# To change other configs, please consult systemd documentation +# +# to activate this service, create a link like: +# $ mkdir -p $HOME/.config/systemd/user && ln -sf $(pwd)/ahoy/tools/rpi/ahoy.service -t $HOME/.config/systemd/user +# $ systemctl --user status ahoy +# $ systemctl --user enable ahoy +# $ systemctl --user start ahoy +# +# 2023.01 +###################################################################### + +[Unit] + +Description=ahoy (lumapu) as Service +After=network.target local-fs.target time-sync.target + +[Service] +ExecStart=/usr/bin/env python3 -um hoymiles --log-transactions --verbose --config ahoy.yml +RestartSec=10 +Restart=on-failure +Type=simple + +# WorkingDirectory must be an absolute path - not relative path +WorkingDirectory=/home/pi/ahoy/tools/rpi +EnvironmentFile=/etc/environment + +[Install] +WantedBy=default.target From eb0f3b319a5f7fe3bce158baf35d5f497e316339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= <122045840+PaeserBastelstube@users.noreply.github.com> Date: Fri, 13 Jan 2023 18:22:41 +0100 Subject: [PATCH 17/23] Delete ahoy.yml --- tools/rpi/ahoy.yml | 84 ---------------------------------------------- 1 file changed, 84 deletions(-) delete mode 100644 tools/rpi/ahoy.yml diff --git a/tools/rpi/ahoy.yml b/tools/rpi/ahoy.yml deleted file mode 100644 index 036ed193..00000000 --- a/tools/rpi/ahoy.yml +++ /dev/null @@ -1,84 +0,0 @@ ---- - -ahoy: - interval: 5 - - logging: - filename: 'RPI-AHOY-DTU.log' - # DEBUG, INFO, WARNING, ERROR, FATAL - level: 'DEBUG' - - sunset: - disabled: false - latitude: 52.485333 - longitude: 10.315783 - altitude: 55 - - # List of available NRF24 transceivers - nrf: - - ce_pin: 22 - cs_pin: 0 - txpower: 'low' # default txpower (min,low,high,max) - - mqtt: - disabled: false - host: 192.168.254.36 - port: 1883 - user: 'Raspberry' - password: 'rpi' - useTLS: False - insecureTLS: False #set True for e.g. self signed certificates. - - # Influx2 output - influxdb: - disabled: true - url: 'http://influxserver.local:8086' - org: 'myorg' - token: '' - bucket: 'telegraf/autogen' - measurement: 'hoymiles' - - volkszaehler: - disabled: true - inverters: - - serial: 114172220003 - url: 'http://localhost/middleware/' - channels: - - type: 'temperature' - uid: 'ad578a40-1d97-11ed-8e8b-fda01a416575' - - type: 'frequency' - uid: '' - - type: 'ac_power0' - uid: '7ca5ac50-1e41-11ed-927f-610c4cb2c69e' - - type: 'ac_voltage0' - uid: '9a38e2e0-1d94-11ed-b539-25f8607ac030' - - type: 'ac_current0' - uid: 'a9a4daf0-1e41-11ed-b68c-eb73eef3d21d' - - type: 'dc_power0' - uid: '38eb3ca0-1e53-11ed-b830-792e70a592fa' - - type: 'dc_voltage0' - uid: '' - - type: 'dc_current0' - uid: '' - - type: 'dc_power1' - uid: '51c0e9d0-1e53-11ed-b574-8bc81547eb8f' - - type: 'dc_voltage1' - uid: '' - - type: 'dc_current1' - uid: '' - - dtu: - serial: 99978563001 - - inverters: - - name: 'Appelweg_PV' # 2ch inverter 1141-Series - serial: 114181807700 # ser-nr starts with 1141... - txpower: 'low' # txpower per inverter (min,low,high,max) - mqtt: - send_raw_enabled: false # allow inject debug data via mqtt - topic: 'Appelweg_PV/114181807700' # defaults to 'hoymiles/{serial}' - strings: # list all available strings - - s_name: 'String 1 left' # String 1 name - s_maxpower: 405 # String 1 max power in Wp - - s_name: 'String 2 right' # String 2 name - s_maxpower: 405 # String 2 max power in Wp From 17c1fec2ad86d23ff0fb27a7e7c9762f7a6bd99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= Date: Sat, 14 Jan 2023 20:49:52 +0100 Subject: [PATCH 18/23] RPi-calculate and print irradiation (Bestrahlung) To calculate irradiation, max-power of each PV module must be defined in ahoy.yml (pls see ahoy.yml.example) --- tools/rpi/ahoy.yml.example | 16 +++++-- tools/rpi/hoymiles/decoders/__init__.py | 63 ++++++++++++++++++++++--- tools/rpi/hoymiles/outputs.py | 17 +++++-- 3 files changed, 83 insertions(+), 13 deletions(-) diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index 04da1f18..7cb0c3b7 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -73,7 +73,17 @@ ahoy: inverters: - name: 'balkon' serial: 114172220003 - txpower: 'low' # txpower per inverter (min,low,high,max) + txpower: 'low' # txpower per inverter (min,low,high,max) mqtt: - send_raw_enabled: false # allow inject debug data via mqtt - topic: 'hoymiles/114172221234' # defaults to 'hoymiles/{serial}' + send_raw_enabled: false # allow inject debug data via mqtt + topic: 'hoymiles/114172221234' # defaults to '{inverter-name}/{serial}' + strings: # list all available strings + - s_name: 'String 1 left' # String 1 name + s_maxpower: 395 # String 1 max power in Wp + - s_name: 'String 2 right' # String 2 name + s_maxpower: 400 # String 2 max power in Wp + - s_name: 'String 3 up' # String 3 name + s_maxpower: 405 # String 3 max power in Wp + - s_name: 'String 4 down' # String 4 name + s_maxpower: 410 # String 4 max power in Wp + diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index b9ebc45e..c379f2e4 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -74,9 +74,11 @@ class Response: 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] + strings = params.get('strings', None) + self.inv_strings = strings + if isinstance(params.get('time_rx', None), datetime): self.time_rx = params['time_rx'] else: @@ -91,7 +93,7 @@ class Response: class StatusResponse(Response): """Inverter StatusResponse object""" - e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor', 'reactive_power'] + e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor', 'reactive_power', 'irradiation'] temperature = None frequency = None powerfactor = None @@ -333,7 +335,7 @@ class HardwareInfoResponse(UnknownResponse): { FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 }, { FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 }, { FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 }, - { FLD_unknown, UNIT_NONE, CH0, 6, 2, 1 }, + { FLD_FW_Build_Hour_Minute, UNIT_NONE, CH0, 6, 2, 1 }, { FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 }, { FLD_unknown, UNIT_NONE, CH0, 10, 2, 1 }, { FLD_unknown, UNIT_NONE, CH0, 12, 2, 1 }, @@ -341,16 +343,21 @@ class HardwareInfoResponse(UnknownResponse): }; self.response = bytes('\x27\x1a\x07\xe5\x04\x4d\x03\x4a\x00\x68\x00\x00\x00\x00\xe6\xfb', 'latin1') """ - fw_version, fw_build_yyyy, fw_build_mmdd, unknown, hw_id = struct.unpack('>HHHHH', self.response[0:10]) + fw_version, fw_build_yyyy, fw_build_mmdd, fw_build_hhmm, hw_id = struct.unpack('>HHHHH', self.response[0:10]) + + responce_info = self.response + logging.debug(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}') fw_version_maj = int((fw_version / 10000)) fw_version_min = int((fw_version % 10000) / 100) fw_version_pat = int((fw_version % 100)) fw_build_mm = int(fw_build_mmdd / 100) fw_build_dd = int(fw_build_mmdd % 100) - logging.debug(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} build at {fw_build_dd}/{fw_build_mm}/{fw_build_yyyy}, HW revision {hw_id}') - responce_info = self.response - logging.debug(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}') + fw_build_HH = int(fw_build_hhmm / 100) + fw_build_MM = int(fw_build_hhmm % 100) + logging.debug(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} '\ + f'build at {fw_build_dd:>02}/{fw_build_mm:>02}/{fw_build_yyyy}T{fw_build_HH:>02}:{fw_build_MM:>02}, '\ + f'HW revision {hw_id}') class DebugDecodeAny(UnknownResponse): """Default decoder""" @@ -426,6 +433,12 @@ class Hm300Decode0B(StatusResponse): def dc_energy_daily_0(self): """ String 1 daily energy in Wh """ return self.unpack('>H', 12)[0] + @property + def dc_irradiation_0(self): + """ String 1 irratiation in percent """ + if self.inv_strings is None: + return None + return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3) @property def ac_voltage_0(self): @@ -492,6 +505,12 @@ class Hm600Decode0B(StatusResponse): def dc_energy_daily_0(self): """ String 1 daily energy in Wh """ return self.unpack('>H', 22)[0] + @property + def dc_irradiation_0(self): + """ String 1 irratiation in percent """ + if self.inv_strings is None: + return None + return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3) @property def dc_voltage_1(self): @@ -513,6 +532,12 @@ class Hm600Decode0B(StatusResponse): def dc_energy_daily_1(self): """ String 2 daily energy in Wh """ return self.unpack('>H', 24)[0] + @property + def dc_irradiation_1(self): + """ String 2 irratiation in percent """ + if self.inv_strings is None: + return None + return round(self.unpack('>H', 12)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3) @property def ac_voltage_0(self): @@ -587,6 +612,12 @@ class Hm1200Decode0B(StatusResponse): def dc_energy_daily_0(self): """ String 1 daily energy in Wh """ return self.unpack('>H', 20)[0] + @property + def dc_irradiation_0(self): + """ String 1 irratiation in percent """ + if self.inv_strings is None: + return None + return round(self.unpack('>H', 8)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3) @property def dc_voltage_1(self): @@ -608,6 +639,12 @@ class Hm1200Decode0B(StatusResponse): def dc_energy_daily_1(self): """ String 2 daily energy in Wh """ return self.unpack('>H', 22)[0] + @property + def dc_irradiation_0(self): + """ String 2 irratiation in percent """ + if self.inv_strings is None: + return None + return round(self.unpack('>H', 10)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3) @property def dc_voltage_2(self): @@ -629,6 +666,12 @@ class Hm1200Decode0B(StatusResponse): def dc_energy_daily_2(self): """ String 3 daily energy in Wh """ return self.unpack('>H', 42)[0] + @property + def dc_irradiation_0(self): + """ String 3 irratiation in percent """ + if self.inv_strings is None: + return None + return round(self.unpack('>H', 30)[0]/10/self.inv_strings[2]['s_maxpower']*100, 3) @property def dc_voltage_3(self): @@ -650,6 +693,12 @@ class Hm1200Decode0B(StatusResponse): def dc_energy_daily_3(self): """ String 4 daily energy in Wh """ return self.unpack('>H', 44)[0] + @property + def dc_irradiation_0(self): + """ String 4 irratiation in percent """ + if self.inv_strings is None: + return None + return round(self.unpack('>H', 32)[0]/10/self.inv_strings[3]['s_maxpower']*100, 3) @property def ac_voltage_0(self): diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index 4913f145..e70c3b8d 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -115,14 +115,18 @@ 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}') 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=temperature value={data["temperature"]:.2f} {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}') @@ -205,15 +209,19 @@ class MqttOutputPlugin(OutputPluginFactory): 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']) string_id = string_id + 1 # Global 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}/temperature', data['temperature']) + + 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) @@ -265,13 +273,16 @@ class VzInverterOutput: 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']) string_id = string_id + 1 + # Global if data['powerfactor'] is not None: self.try_publish(ts, f'powerfactor', data['powerfactor']) self.try_publish(ts, f'frequency', data['frequency']) - self.try_publish(ts, f'temperature', data['temperature']) + self.try_publish(ts, f'Temp', data['temperature']) if data['energy_total'] is not None: self.try_publish(ts, f'total', data['energy_total']) From ccca67d5c4bb682cd4c97a55df7807fc9480983f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knuti=5Fin=5FP=C3=A4se?= Date: Sun, 15 Jan 2023 20:19:38 +0100 Subject: [PATCH 19/23] RPi: update description on ahoy.service Automatic operation of Ahoy on an RPi can be based on the systemd method. To activate systemd processes for ahoy, the config ahoy.service is needed. --- tools/rpi/ahoy.service | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tools/rpi/ahoy.service b/tools/rpi/ahoy.service index 68b0a121..394bc09e 100644 --- a/tools/rpi/ahoy.service +++ b/tools/rpi/ahoy.service @@ -1,22 +1,24 @@ ###################################################################### # systemd.service configuration for ahoy (lumapu) # users can modify the lines: -# - Description -# - ExecStart (for example: name of config file) -# - WorkingDirectory -# To change other configs, please consult systemd documentation +# Description +# ExecStart (example: name of config file) +# WorkingDirectory (absolute path to your private ahoy dir) +# To change other config parameter, please consult systemd documentation # -# to activate this service, create a link like: -# $ mkdir -p $HOME/.config/systemd/user && ln -sf $(pwd)/ahoy/tools/rpi/ahoy.service -t $HOME/.config/systemd/user +# To activate this service, create a link, enable and start the ahoy.service +# $ mkdir -p $HOME/.config/systemd/user +# $ ln -sf $(pwd)/ahoy/tools/rpi/ahoy.service -t $HOME/.config/systemd/user # $ systemctl --user status ahoy # $ systemctl --user enable ahoy # $ systemctl --user start ahoy +# $ systemctl --user status ahoy # # 2023.01 ###################################################################### [Unit] - + Description=ahoy (lumapu) as Service After=network.target local-fs.target time-sync.target @@ -32,3 +34,4 @@ EnvironmentFile=/etc/environment [Install] WantedBy=default.target + From 6e41a83a26bed882bb2dab0508006b21533e9b01 Mon Sep 17 00:00:00 2001 From: lumapu Date: Sun, 8 Jan 2023 22:16:14 +0100 Subject: [PATCH 20/23] corrected MQTT `comm_disabled` #529 fix Prometheus and JSON endpoints (`config_override.h`) #561 publish MQTT with fixed interval even if inverter is not available #542 added JSON settings upload. NOTE: settings JSON download changed, so only settings should be uploaded starting from version `0.5.70` #551 MQTT topic and inverter name have more allowed characters: `[A-Za-z0-9./#$%&=+_-]+`, thx: @Mo Demman improved potential issue with `checkTicker`, thx @cbscpe MQTT option for reset values on midnight / not avail / communication stop #539 small fix in `tickIVCommunication` #534 add `YieldTotal` correction, eg. to have the option to zero at year start #512 --- src/CHANGES.md | 8 +++ src/app.cpp | 14 +++- src/app.h | 7 +- src/appInterface.h | 1 + src/config/settings.h | 31 +++++++-- src/defines.h | 2 +- src/hm/hmInverter.h | 18 ++++- src/publisher/pubMqtt.h | 143 +++++++++++++++++++++++++++++---------- src/utils/ahoyTimer.h | 2 +- src/utils/llist.h | 110 ------------------------------ src/utils/scheduler.h | 1 + src/web/RestApi.h | 43 +++++++++--- src/web/html/setup.html | 45 +++++++++--- src/web/html/update.html | 1 - src/web/web.h | 129 ++++++++++++++++++++++++++--------- src/wifi/ahoywifi.h | 3 +- 16 files changed, 343 insertions(+), 215 deletions(-) delete mode 100644 src/utils/llist.h diff --git a/src/CHANGES.md b/src/CHANGES.md index 364b2b4d..ce304e4b 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -4,6 +4,14 @@ ## 0.5.70 * corrected MQTT `comm_disabled` #529 +* fix Prometheus and JSON endpoints (`config_override.h`) #561 +* publish MQTT with fixed interval even if inverter is not available #542 +* added JSON settings upload. NOTE: settings JSON download changed, so only settings should be uploaded starting from version `0.5.70` #551 +* MQTT topic and inverter name have more allowed characters: `[A-Za-z0-9./#$%&=+_-]+`, thx: @Mo Demman +* improved potential issue with `checkTicker`, thx @cbscpe +* MQTT option for reset values on midnight / not avail / communication stop #539 +* small fix in `tickIVCommunication` #534 +* add `YieldTotal` correction, eg. to have the option to zero at year start #512 ## 0.5.69 * merged SH1106 1.3" Display, thx @dAjaY85 diff --git a/src/app.cpp b/src/app.cpp index 6f53121b..e42b8c2b 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -62,6 +62,9 @@ void app::setup() { if (mConfig->mqtt.broker[0] > 0) { everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt)); everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt)); + uint32_t nxtTrig = mTimestamp - ((mTimestamp - 1) % 86400) + 86400; // next midnight + if(mConfig->mqtt.rstYieldMidNight) + onceAt(std::bind(&app::tickMidnight, this), nxtTrig); mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1)); } #endif @@ -161,7 +164,7 @@ void app::tickIVCommunication(void) { if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start nxtTrig = mSunrise - mConfig->sun.offsetSec; } else { - if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise + if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise nxtTrig = 0; } else { // current time lies within communication start/stop time, set next trigger to communication stop mIVCommunicationOn = true; @@ -207,6 +210,15 @@ void app::tickSend(void) { updateLed(); } +//----------------------------------------------------------------------------- +void app::tickMidnight(void) { + // only used and enabled by MQTT (see setup()) + uint32_t nxtTrig = mTimestamp - ((mTimestamp - 1) % 86400) + 86400; // next midnight + onceAt(std::bind(&app::tickMidnight, this), nxtTrig); + + mMqtt.tickerMidnight(); +} + //----------------------------------------------------------------------------- void app::handleIntr(void) { DPRINTLN(DBG_VERBOSE, F("app::handleIntr")); diff --git a/src/app.h b/src/app.h index b08b7d05..fdbf9616 100644 --- a/src/app.h +++ b/src/app.h @@ -78,6 +78,10 @@ class app : public IApp, public ah::Scheduler { return mSettings.saveSettings(); } + bool readSettings(const char *path) { + return mSettings.readSettings(path); + } + bool eraseSettings(bool eraseWifi = false) { return mSettings.eraseSettings(eraseWifi); } @@ -95,7 +99,7 @@ class app : public IApp, public ah::Scheduler { } void setRebootFlag() { - once(std::bind(&app::tickReboot, this), 1); + once(std::bind(&app::tickReboot, this), 3); } const char *getVersion() { @@ -203,6 +207,7 @@ class app : public IApp, public ah::Scheduler { void tickCalcSunrise(void); void tickIVCommunication(void); void tickSend(void); + void tickMidnight(void); /*void tickSerial(void) { if(Serial.available() == 0) return; diff --git a/src/appInterface.h b/src/appInterface.h index 4c25c3a2..c2d191b2 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -15,6 +15,7 @@ class IApp { public: virtual ~IApp() {} virtual bool saveSettings() = 0; + virtual bool readSettings(const char *path) = 0; virtual bool eraseSettings(bool eraseWifi) = 0; virtual void setRebootFlag() = 0; virtual const char *getVersion() = 0; diff --git a/src/config/settings.h b/src/config/settings.h index 921ebc96..50fbe01d 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -97,6 +97,9 @@ typedef struct { char pwd[MQTT_PWD_LEN]; char topic[MQTT_TOPIC_LEN]; uint16_t interval; + bool rstYieldMidNight; + bool rstValsNotAvail; + bool rstValsCommStop; } cfgMqtt_t; typedef struct { @@ -105,6 +108,7 @@ typedef struct { serial_u serial; uint16_t chMaxPwr[4]; char chName[4][MAX_NAME_LENGTH]; + uint32_t yieldCor; // YieldTotal correction value } cfgIv_t; typedef struct { @@ -155,7 +159,7 @@ class settings { else DPRINTLN(DBG_INFO, F(" .. done")); - readSettings(); + readSettings("/settings.json"); } // should be used before OTA @@ -186,9 +190,10 @@ class settings { #endif } - void readSettings(void) { + bool readSettings(const char* path) { + bool success = false; loadDefaults(); - File fp = LittleFS.open("/settings.json", "r"); + File fp = LittleFS.open(path, "r"); if(!fp) DPRINTLN(DBG_WARN, F("failed to load json, using default config")); else { @@ -206,6 +211,7 @@ class settings { jsonMqtt(root["mqtt"]); jsonLed(root["led"]); jsonInst(root["inst"]); + success = true; } else { Serial.println(F("failed to parse json, using default config")); @@ -213,6 +219,7 @@ class settings { fp.close(); } + return success; } bool saveSettings(void) { @@ -299,6 +306,9 @@ class settings { snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD); snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); mCfg.mqtt.interval = 0; // off + mCfg.mqtt.rstYieldMidNight = false; + mCfg.mqtt.rstValsNotAvail = false; + mCfg.mqtt.rstValsCommStop = false; mCfg.led.led0 = DEF_LED0_PIN; mCfg.led.led1 = DEF_LED1_PIN; @@ -399,9 +409,16 @@ class settings { obj[F("pwd")] = mCfg.mqtt.pwd; obj[F("topic")] = mCfg.mqtt.topic; obj[F("intvl")] = mCfg.mqtt.interval; + obj[F("rstMidNight")] = (bool)mCfg.mqtt.rstYieldMidNight; + obj[F("rstNotAvail")] = (bool)mCfg.mqtt.rstValsNotAvail; + obj[F("rstComStop")] = (bool)mCfg.mqtt.rstValsCommStop; + } else { mCfg.mqtt.port = obj[F("port")]; mCfg.mqtt.interval = obj[F("intvl")]; + mCfg.mqtt.rstYieldMidNight = (bool)obj["rstMidNight"]; + mCfg.mqtt.rstValsNotAvail = (bool)obj["rstNotAvail"]; + mCfg.mqtt.rstValsCommStop = (bool)obj["rstComStop"]; snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", obj[F("broker")].as()); snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", obj[F("user")].as()); snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", obj[F("pwd")].as()); @@ -438,9 +455,10 @@ class settings { void jsonIv(JsonObject obj, cfgIv_t *cfg, bool set = false) { if(set) { - obj[F("en")] = (bool)cfg->enabled; - obj[F("name")] = cfg->name; - obj[F("sn")] = cfg->serial.u64; + obj[F("en")] = (bool)cfg->enabled; + obj[F("name")] = cfg->name; + obj[F("sn")] = cfg->serial.u64; + obj[F("yield")] = cfg->yieldCor; for(uint8_t i = 0; i < 4; i++) { obj[F("pwr")][i] = cfg->chMaxPwr[i]; obj[F("chName")][i] = cfg->chName[i]; @@ -449,6 +467,7 @@ class settings { cfg->enabled = (bool)obj[F("en")]; snprintf(cfg->name, MAX_NAME_LENGTH, "%s", obj[F("name")].as()); cfg->serial.u64 = obj[F("sn")]; + cfg->yieldCor = obj[F("yield")]; for(uint8_t i = 0; i < 4; i++) { cfg->chMaxPwr[i] = obj[F("pwr")][i]; snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as()); diff --git a/src/defines.h b/src/defines.h index b37cb6c7..8ab9ccda 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 5 -#define VERSION_PATCH 69 +#define VERSION_PATCH 70 //------------------------------------- typedef struct { diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 8c1fccc8..f02c6b9b 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -233,11 +233,13 @@ class Inverter { val <<= 8; val |= buf[ptr]; } while(++ptr != end); - if(FLD_T == rec->assign[pos].fieldId) { + if (FLD_T == rec->assign[pos].fieldId) { // temperature is a signed value! rec->record[pos] = (REC_TYP)((int16_t)val) / (REC_TYP)(div); - } - else { + } else if ((FLD_YT == rec->assign[pos].fieldId) + && (config->yieldCor != 0)) { + rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div) - (REC_TYP)config->yieldCor; + } else { if ((REC_TYP)(div) > 1) rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div); else @@ -286,6 +288,16 @@ class Inverter { DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x")); } + bool setValue(uint8_t pos, record_t<> *rec, REC_TYP val) { + DPRINTLN(DBG_VERBOSE, F("hmInverter.h:setValue")); + if(NULL == rec) + return false; + if(pos > rec->length) + return false; + rec->record[pos] = val; + return true; + } + REC_TYP getValue(uint8_t pos, record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getValue")); if(NULL == rec) diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index 382406f4..85742bd5 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -112,6 +112,27 @@ class PubMqtt { void tickerComm(bool disabled) { publish("comm_disabled", ((disabled) ? "true" : "false"), true); publish("comm_dis_ts", String(*mUtcTimestamp).c_str(), true); + + if(disabled && (mCfgMqtt->rstValsCommStop)) + zeroAllInverters(); + } + + void tickerMidnight() { + Inverter<> *iv; + record_t<> *rec; + + // set YieldDay to zero + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + rec = iv->getRecordStruct(RealTimeRunData_Debug); + uint8_t pos = iv->getPosByChFld(CH0, FLD_YD, rec); + iv->setValue(pos, rec, 0.0f); + } + + mSendList.push(RealTimeRunData_Debug); + sendIvData(); } void payloadEventListener(uint8_t cmd) { @@ -394,18 +415,21 @@ class PubMqtt { allAvail = false; } } - else if (!iv->isProducing(*mUtcTimestamp, rec)) { + else { mIvAvail = true; - if (MQTT_STATUS_AVAIL_PROD == status) - status = MQTT_STATUS_AVAIL_NOT_PROD; + if (!iv->isProducing(*mUtcTimestamp, rec)) { + if (MQTT_STATUS_AVAIL_PROD == status) + status = MQTT_STATUS_AVAIL_NOT_PROD; + } } - else - mIvAvail = true; if(mLastIvState[id] != status) { mLastIvState[id] = status; changed = true; + if(mCfgMqtt->rstValsNotAvail) + zeroValues(iv); + snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name); snprintf(val, 40, "%d", status); publish(topic, val, true); @@ -419,12 +443,13 @@ class PubMqtt { if(changed) { snprintf(val, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((mIvAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE))); publish("status", val, true); + sendIvData(false); // false prevents loop of same function } return totalComplete; } - void sendIvData(void) { + void sendIvData(bool sendTotals = true) { if(mSendList.empty()) return; @@ -442,49 +467,52 @@ class PubMqtt { record_t<> *rec = iv->getRecordStruct(mSendList.front()); // data - if(iv->isAvailable(*mUtcTimestamp, rec)) { - for (uint8_t i = 0; i < rec->length; i++) { - bool retained = false; - if (mSendList.front() == RealTimeRunData_Debug) { + //if(iv->isAvailable(*mUtcTimestamp, rec) || (0 != mCfgMqtt->interval)) { // is avail or fixed pulish interval was set + for (uint8_t i = 0; i < rec->length; i++) { + bool retained = false; + if (mSendList.front() == RealTimeRunData_Debug) { + switch (rec->assign[i].fieldId) { + case FLD_YT: + case FLD_YD: + retained = true; + break; + } + } + + snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]); + snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec))); + publish(topic, val, retained); + + // calculate total values for RealTimeRunData_Debug + if (mSendList.front() == RealTimeRunData_Debug) { + if (CH0 == rec->assign[i].ch) { switch (rec->assign[i].fieldId) { + case FLD_PAC: + total[0] += iv->getValue(i, rec); + break; case FLD_YT: + total[1] += iv->getValue(i, rec); + break; case FLD_YD: - retained = true; + total[2] += iv->getValue(i, rec); + break; + case FLD_PDC: + total[3] += iv->getValue(i, rec); break; } } - - snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]); - snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec))); - publish(topic, val, retained); - - // calculate total values for RealTimeRunData_Debug - if (mSendList.front() == RealTimeRunData_Debug) { - if (CH0 == rec->assign[i].ch) { - switch (rec->assign[i].fieldId) { - case FLD_PAC: - total[0] += iv->getValue(i, rec); - break; - case FLD_YT: - total[1] += iv->getValue(i, rec); - break; - case FLD_YD: - total[2] += iv->getValue(i, rec); - break; - case FLD_PDC: - total[3] += iv->getValue(i, rec); - break; - } - } - sendTotal = true; - } - yield(); + sendTotal = true; } + yield(); } + //} } mSendList.pop(); // remove from list once all inverters were processed + if(!sendTotals) // skip total value calculation + continue; + if ((true == sendTotal) && processIvStatus()) { uint8_t fieldId; for (uint8_t i = 0; i < 4; i++) { @@ -511,6 +539,47 @@ class PubMqtt { } } + void zeroAllInverters() { + Inverter<> *iv; + + // set values to zero, exept yields + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + zeroValues(iv); + } + sendIvData(); + } + + void zeroValues(Inverter<> *iv) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + for(uint8_t ch = 0; ch <= iv->channels; ch++) { + uint8_t pos = 0; + uint8_t fld = 0; + while(0xff != pos) { + switch(fld) { + case FLD_YD: + case FLD_YT: + case FLD_FW_VERSION: + case FLD_FW_BUILD_YEAR: + case FLD_FW_BUILD_MONTH_DAY: + case FLD_FW_BUILD_HOUR_MINUTE: + case FLD_HW_ID: + case FLD_ACT_ACTIVE_PWR_LIMIT: + continue; + break; + } + pos = iv->getPosByChFld(ch, fld, rec); + iv->setValue(pos, rec, 0.0f); + fld++; + } + } + + mSendList.push(RealTimeRunData_Debug); + } + espMqttClient mClient; cfgMqtt_t *mCfgMqtt; #if defined(ESP8266) diff --git a/src/utils/ahoyTimer.h b/src/utils/ahoyTimer.h index 5c960a34..08c09016 100644 --- a/src/utils/ahoyTimer.h +++ b/src/utils/ahoyTimer.h @@ -15,7 +15,7 @@ namespace ah { *ticker = mil + interval; return true; } - else if(mil < (*ticker - interval)) { + else if((mil + interval) < (*ticker)) { *ticker = mil + interval; return true; } diff --git a/src/utils/llist.h b/src/utils/llist.h deleted file mode 100644 index 69750f19..00000000 --- a/src/utils/llist.h +++ /dev/null @@ -1,110 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://ahoydtu.de -// Lukas Pusch, lukas@lpusch.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- -#ifndef __LIST_H__ -#define __LIST_H__ - -template -struct node_s { - typedef T dT; - node_s *pre; - node_s *nxt; - uint8_t id; - dT d; - node_s() : pre(NULL), nxt(NULL), d() {} - node_s(Args... args) : id(0), pre(NULL), nxt(NULL), d(args...) {} -}; - -template -class llist { - typedef node_s elmType; - typedef T dataType; - public: - llist() : root(mPool) { - root = NULL; - elmType *p = mPool; - for(uint32_t i = 0; i < MAX_NUM; i++) { - p->id = i; - p++; - } - mFill = mMax = 0; - } - - elmType *add(Args... args) { - elmType *p = root, *t; - if(NULL == (t = getFreeNode())) - return NULL; - if(++mFill > mMax) - mMax = mFill; - - if(NULL == root) { - p = root = t; - p->pre = p; - p->nxt = p; - } - else { - p = root->pre; - t->pre = p; - p->nxt->pre = t; - t->nxt = p->nxt; - p->nxt = t; - } - t->d = dataType(args...); - return p; - } - - elmType *getFront() { - return root; - } - - elmType *get(elmType *p) { - p = p->nxt; - return (p == root) ? NULL : p; - } - - elmType *rem(elmType *p) { - if(NULL == p) - return NULL; - elmType *t = p->nxt; - p->nxt->pre = p->pre; - p->pre->nxt = p->nxt; - if((root == p) && (p->nxt == p)) - root = NULL; - else - root = p->nxt; - p->nxt = NULL; - p->pre = NULL; - p = NULL; - mFill--; - return (NULL == root) ? NULL : ((t == root) ? NULL : t); - } - - uint16_t getFill(void) { - return mFill; - } - - uint16_t getMaxFill(void) { - return mMax; - } - - protected: - elmType *root; - - private: - elmType *getFreeNode(void) { - elmType *n = mPool; - for(uint32_t i = 0; i < MAX_NUM; i++) { - if(NULL == n->nxt) - return n; - n++; - } - return NULL; - } - - elmType mPool[MAX_NUM]; - uint16_t mFill, mMax; -}; - -#endif /*__LIST_H__*/ diff --git a/src/utils/scheduler.h b/src/utils/scheduler.h index 36dcdaae..330ab080 100644 --- a/src/utils/scheduler.h +++ b/src/utils/scheduler.h @@ -129,6 +129,7 @@ namespace ah { mTickerInUse[i] = false; else mTicker[i].timeout = mTicker[i].reload; + //DPRINTLN(DBG_INFO, "checkTick " + String(i) + " reload: " + String(mTicker[i].reload) + ", timeout: " + String(mTicker[i].timeout)); (mTicker[i].c)(); yield(); } diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 8986e76d..07c12994 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -134,17 +134,34 @@ class RestApi { ep[F("record/config")] = url + F("record/config"); ep[F("record/live")] = url + F("record/live"); } + void onDwnldSetup(AsyncWebServerRequest *request) { - AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); - JsonObject root = response->getRoot(); + AsyncWebServerResponse *response; - getSetup(root); + File fp = LittleFS.open("/settings.json", "r"); + if(!fp) { + DPRINTLN(DBG_ERROR, F("failed to load settings")); + response = request->beginResponse(200, F("application/json"), "{}"); + } + else { + String tmp = fp.readString(); + int i = 0; + // remove all passwords + while (i != -1) { + i = tmp.indexOf("\"pwd\":", i); + if(-1 != i) { + i+=7; + tmp.remove(i, tmp.indexOf("\"", i)-i); + } + } + response = request->beginResponse(200, F("application/json"), tmp); + } - response->setLength(); response->addHeader("Content-Type", "application/octet-stream"); response->addHeader("Content-Description", "File Transfer"); response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json"); request->send(response); + fp.close(); } void getGeneric(JsonObject obj) { @@ -165,7 +182,7 @@ class RestApi { obj[F("device_name")] = mConfig->sys.deviceName; obj[F("mac")] = WiFi.macAddress(); - obj[F("hostname")] = WiFi.getHostname(); + obj[F("hostname")] = mConfig->sys.deviceName; obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0); obj[F("prot_mask")] = mConfig->sys.protectionMask; @@ -263,6 +280,7 @@ class RestApi { obj2[F("serial")] = String(iv->config->serial.u64, HEX); obj2[F("channels")] = iv->channels; obj2[F("version")] = String(iv->getFwVersion()); + obj2[F("yieldCor")] = iv->config->yieldCor; for(uint8_t j = 0; j < iv->channels; j ++) { obj2[F("ch_max_power")][j] = iv->config->chMaxPwr[j]; @@ -276,12 +294,15 @@ class RestApi { } void getMqtt(JsonObject obj) { - obj[F("broker")] = String(mConfig->mqtt.broker); - obj[F("port")] = String(mConfig->mqtt.port); - obj[F("user")] = String(mConfig->mqtt.user); - obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); - obj[F("topic")] = String(mConfig->mqtt.topic); - obj[F("interval")] = String(mConfig->mqtt.interval); + obj[F("broker")] = String(mConfig->mqtt.broker); + obj[F("port")] = String(mConfig->mqtt.port); + obj[F("user")] = String(mConfig->mqtt.user); + obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); + obj[F("topic")] = String(mConfig->mqtt.topic); + obj[F("interval")] = String(mConfig->mqtt.interval); + obj[F("rstMid")] = (bool)mConfig->mqtt.rstYieldMidNight; + obj[F("rstNAvail")] = (bool)mConfig->mqtt.rstValsNotAvail; + obj[F("rstComStop")] = (bool)mConfig->mqtt.rstValsCommStop; } void getNtp(JsonObject obj) { diff --git a/src/web/html/setup.html b/src/web/html/setup.html index c60020e1..aad46232 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -31,7 +31,13 @@
ERASE SETTINGS (not WiFi) - +
+ Upload JSON Settings +
+ + +
+
Device Host Name @@ -148,6 +154,12 @@ + +
+ +
+ +

Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)

@@ -184,7 +196,7 @@
- Download your settings (JSON file) (only saved values) + Download your settings (JSON file) (only saved values, passwords will be removed!)
@@ -212,8 +224,9 @@ const re = /11[2,4,6]1.*/; document.getElementById("btnAdd").addEventListener("click", function() { - if(highestId <= (maxInv-1)) - ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_power":[0,0,0,0],"ch_name":["","","",""]}'), highestId + 1); + if(highestId <= (maxInv-1)) { + ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_power":[0,0,0,0],"ch_name":["","","",""]}'), highestId); + } }); function apiCbWifi(obj) { @@ -268,6 +281,12 @@ getAjax("/api/setup", apiCbMqtt, "POST", JSON.stringify(obj)); } + function hide() { + document.getElementById("form").submit(); + var e = document.getElementById("content"); + e.replaceChildren(span("upload started")); + } + function delIv() { var id = this.id.substring(0,4); var e = document.getElementsByName(id + "Addr")[0]; @@ -278,8 +297,8 @@ } function ivHtml(obj, id) { - highestId = id; - if(highestId == (maxInv - 1)) + highestId = id + 1; + if(highestId == maxInv) setHide("btnAdd", true); iv = document.getElementById("inverter"); iv.appendChild(des("Inverter " + id)); @@ -292,7 +311,7 @@ iv.appendChild(br()); iv.appendChild(lbl(id + "Addr", "Serial Number (12 digits)*")); - var addr = inp(id + "Addr", obj["serial"], 12); + var addr = inp(id + "Addr", obj["serial"], 12, ["text"], null, "text", "[0-9]+", "Invalid input"); iv.appendChild(addr); ['keyup', 'change'].forEach(function(evt) { addr.addEventListener(evt, (e) => { @@ -323,7 +342,7 @@ iv.append( lbl(id + "Name", "Name*"), - inp(id + "Name", obj["name"], 32, ["text"], null, "text", "[A-Za-z0-9.\\-_\\+\\/]+", "Invalid input") + inp(id + "Name", obj["name"], 32, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input") ); for(var j of [["ModPwr", "ch_max_power", "Max Module Power (Wp)", 4, "[0-9]+"], ["ModName", "ch_name", "Module Name", 16, null]]) { @@ -339,10 +358,15 @@ iv.appendChild(d); } + iv.append( + br(), + lbl(id + "YieldCor", "Yield Total Correction (will be subtracted) [kWh]"), + inp(id + "YieldCor", obj["yieldCor"], 32, ["text"], null, "text", "[0-9]+", "Invalid input") + ); + var del = inp(id+"del", "X", 0, ["btn", "btnDel"], id+"del", "button"); del.addEventListener("click", delIv); iv.append( - br(), lbl(id + "lbldel", "Delete"), del ); @@ -394,6 +418,9 @@ function parseMqtt(obj) { for(var i of [["Addr", "broker"], ["Port", "port"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"], ["Interval", "interval"]]) document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]]; + + for(var i of [["Mid", "rstMid"], ["ComStop", "rstNAvail"], ["NotAvail", "rstComStop"]]) + document.getElementsByName("mqttRst"+i[0])[0].checked = obj[i[1]]; } function parseNtp(obj) { diff --git a/src/web/html/update.html b/src/web/html/update.html index 215188db..e9bcde87 100644 --- a/src/web/html/update.html +++ b/src/web/html/update.html @@ -23,7 +23,6 @@ -