From ba0666582a3cdd24b532912ab32f0687dc4fea92 Mon Sep 17 00:00:00 2001 From: Johan Meijer <16508298+johanmeijer@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:38:41 +0100 Subject: [PATCH 01/17] Add T065120 to support Smart meter Add T065120 to support Smart meter with device number 51 --- examples/Record Layout/T065120.json | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 examples/Record Layout/T065120.json diff --git a/examples/Record Layout/T065120.json b/examples/Record Layout/T065120.json new file mode 100644 index 00000000..69b0177a --- /dev/null +++ b/examples/Record Layout/T065120.json @@ -0,0 +1,41 @@ +{"T065120": { + "decrypt" : {"value" :"True"}, + "datalogserial" : {"value" :16, "length" : 10, "type" : "text", "divide" : 10}, + "pvserial" : {"value" :76, "length" : 10, "type" : "text", "divide" : 10}, + "date" : {"value" :136, "divide" : 10}, + "voltage_l1" : {"value" :160, "length" : 4, "type" : "num", "divide" : 10}, + "voltage_l2" : {"value" :168, "length" : 4, "type" : "num", "divide" : 10,"incl" : "no"}, + "voltage_l3" : {"value" :176, "length" : 4, "type" : "num", "divide" : 10,"incl" : "no"}, + "Current_l1" : {"value" :184, "length" : 4, "type" : "num", "divide" : 10}, + "Current_l2" : {"value" :192, "length" : 4, "type" : "num", "divide" : 10,"incl" : "no"}, + "Current_l3" : {"value" :200, "length" : 4, "type" : "num", "divide" : 10,"incl" : "no"}, + "act_power_l1" : {"value" :208, "length" : 4, "type" : "numx", "divide" : 10}, + "act_power_l2" : {"value" :216, "length" : 4, "type" : "numx", "divide" : 10,"incl" : "no"}, + "act_power_l3" : {"value" :224, "length" : 4, "type" : "numx", "divide" : 10,"incl" : "no"}, + "app_power_l1" : {"value" :232, "length" : 4, "type" : "numx", "divide" : 10}, + "app_power_l2" : {"value" :240, "length" : 4, "type" : "numx", "divide" : 10,"incl" : "no"}, + "app_power_l3" : {"value" :248, "length" : 4, "type" : "numx", "divide" : 10,"incl" : "no"}, + "react_power_l1" : {"value" :256, "length" : 4, "type" : "numx","divide" : 10}, + "react_power_l2" : {"value" :264, "length" : 4, "type" : "numx","divide" : 10,"incl" : "no"}, + "react_power_l3" : {"value" :272, "length" : 4, "type" : "numx","divide" : 10,"incl" : "no"}, + "powerfactor_l1" : {"value" :280, "length" : 4, "type" : "numx", "divide" : 1000}, + "powerfactor_l2" : {"value" :288, "length" : 4, "type" : "numx", "divide" : 1000,"incl" : "no"}, + "powerfactor_l3" : {"value" :296, "length" : 4, "type" : "numx", "divide" : 1000,"incl" : "no"}, + "pos_rev_act_power" : {"value" :304, "length" : 4, "type" : "numx", "divide" : 10}, + "pos_act_power" : {"value" :304, "length" : 4, "type" : "numx", "divide" : 10,"incl" : "no"}, + "rev_act_power" : {"value" :304, "length" : 4, "type" : "numx", "divide" : 10,"incl" : "no"}, + "app_power" : {"value" :312, "length" : 4, "type" : "numx", "divide" : 10}, + "react_power" : {"value" :320, "length" : 4, "type" : "numx", "divide" : 10}, + "powerfactor" : {"value" :328, "length" : 4, "type" : "numx", "divide" : 1000}, + "frequency" : {"value" :336, "length" : 4, "type" : "num", "divide" : 10}, + "L1-2_voltage" : {"value" :344, "length" : 4, "type" : "num", "divide" : 10}, + "L2-3_voltage" : {"value" :352, "length" : 4, "type" : "num", "divide" : 10}, + "L3-1_voltage" : {"value" :360, "length" : 4, "type" : "num", "divide" : 10}, + "pos_act_energy" : {"value" :368, "length" : 4, "type" : "numx", "divide" : 10}, + "rev_act_energy" : {"value" :376, "length" : 4, "type" : "numx", "divide" : 10}, + "pos_act_energy_kvar" : {"value" :384, "length" : 4, "type" : "numx", "divide" : 10,"incl" : "no"}, + "rev_act_energy_kvar" : {"value" :392, "length" : 4, "type" : "numx", "divide" : 10,"incl" : "no"}, + "app_energy_kvar" : {"value" :400, "length" : 4, "type" : "numx", "divide" : 10,"incl" : "no"}, + "act_energy_kwh" : {"value" :408, "length" : 4, "type" : "numx", "divide" : 10,"incl" : "no"}, + "react_energy_kvar" : {"value" :416, "length" : 4, "type" : "numx", "divide" : 10,"incl" : "no"} + } } \ No newline at end of file From 4d1a67a04ebcc6887b217249d20fb76e14aeaf40 Mon Sep 17 00:00:00 2001 From: Johan Meijer <16508298+johanmeijer@users.noreply.github.com> Date: Tue, 4 Mar 2025 22:27:02 +0100 Subject: [PATCH 02/17] Modified tchntthree.json Modified to support CHINT DTU666 with device numbers 21-24 (see also #636) --- examples/Record Layout/tchntthree.json | 115 ++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/examples/Record Layout/tchntthree.json b/examples/Record Layout/tchntthree.json index 03666e65..6ce2abc4 100644 --- a/examples/Record Layout/tchntthree.json +++ b/examples/Record Layout/tchntthree.json @@ -1,4 +1,4 @@ -{"T06241b": { +{"T06211b": { "decrypt" : {"value" :"True"}, "datalogserial" : {"value" :16, "length" : 10, "type" : "text","incl" : "yes"}, "device" : {"value": "CHNT_THREE","type" : "def"}, @@ -34,4 +34,115 @@ "reverseActiveTodayEnergy" : {"pos" :24, "type" : "log"}, "reverActivePower" : {"pos" :24, "type" : "log"}, "end" : {"pos" :25, "type" : "log","incl" : "no"} - }} \ No newline at end of file + }, +"T06221b": { + "decrypt" : {"value" :"True"}, + "datalogserial" : {"value" :16, "length" : 10, "type" : "text","incl" : "yes"}, + "device" : {"value": "CHNT_THREE","type" : "def"}, + "date" : {"value" :80, "divide" : 10}, + "logstart" : {"value" :96,"type" : "def","incl" : "no"}, + "I1Voltage2" : {"pos" :1, "type" : "log"}, + "I2Voltage3" : {"pos" :2, "type" : "log"}, + "I3Voltage1" : {"pos" :3, "type" : "log"}, + "voltageL1" : {"pos" :4, "type" : "log"}, + "voltageL2" : {"pos" :5, "type" : "log"}, + "voltageL3" : {"pos" :6, "type" : "log"}, + "currentL1" : {"pos" :7, "type" : "log"}, + "currentL2" : {"pos" :8, "type" : "log"}, + "currentL3" : {"pos" :9, "type" : "log"}, + "ReverseActiveEnergy" : {"pos" :10, "type" : "log"}, + "activePowerL1" : {"pos" :11, "type" : "log"}, + "activePowerL2" : {"pos" :12, "type" : "log"}, + "activePowerL3" : {"pos" :13, "type" : "log"}, + "reactive_power" : {"pos" :14, "type" : "logneg"}, + "active_power" : {"pos" :14, "type" : "logpos"}, + "reactive_powerL1" : {"pos" :15, "type" : "log"}, + "reactive_powerL2" : {"pos" :16, "type" : "log"}, + "reactive_powerL3" : {"pos" :17, "type" : "log"}, + "power_factor" : {"pos" :18, "type" : "log"}, + "power_factorL1" : {"pos" :19, "type" : "log"}, + "power_factorL2" : {"pos" :20, "type" : "log"}, + "power_factorL3" : {"pos" :21, "type" : "log"}, + "frequency" : {"pos" :22, "type" : "log"}, + "active_energy" : {"pos" :23, "type" : "log"}, + "positiveActiveTodayEnergy" : {"pos" :23, "type" : "log"}, + "posiActivePower" : {"pos" :23, "type" : "log"}, + "reactive_energy" : {"pos" :24, "type" : "log"}, + "reverseActiveTodayEnergy" : {"pos" :24, "type" : "log"}, + "reverActivePower" : {"pos" :24, "type" : "log"}, + "end" : {"pos" :25, "type" : "log","incl" : "no"} + }, +"T06231b": { + "decrypt" : {"value" :"True"}, + "datalogserial" : {"value" :16, "length" : 10, "type" : "text","incl" : "yes"}, + "device" : {"value": "CHNT_THREE","type" : "def"}, + "date" : {"value" :80, "divide" : 10}, + "logstart" : {"value" :96,"type" : "def","incl" : "no"}, + "I1Voltage2" : {"pos" :1, "type" : "log"}, + "I2Voltage3" : {"pos" :2, "type" : "log"}, + "I3Voltage1" : {"pos" :3, "type" : "log"}, + "voltageL1" : {"pos" :4, "type" : "log"}, + "voltageL2" : {"pos" :5, "type" : "log"}, + "voltageL3" : {"pos" :6, "type" : "log"}, + "currentL1" : {"pos" :7, "type" : "log"}, + "currentL2" : {"pos" :8, "type" : "log"}, + "currentL3" : {"pos" :9, "type" : "log"}, + "ReverseActiveEnergy" : {"pos" :10, "type" : "log"}, + "activePowerL1" : {"pos" :11, "type" : "log"}, + "activePowerL2" : {"pos" :12, "type" : "log"}, + "activePowerL3" : {"pos" :13, "type" : "log"}, + "reactive_power" : {"pos" :14, "type" : "logneg"}, + "active_power" : {"pos" :14, "type" : "logpos"}, + "reactive_powerL1" : {"pos" :15, "type" : "log"}, + "reactive_powerL2" : {"pos" :16, "type" : "log"}, + "reactive_powerL3" : {"pos" :17, "type" : "log"}, + "power_factor" : {"pos" :18, "type" : "log"}, + "power_factorL1" : {"pos" :19, "type" : "log"}, + "power_factorL2" : {"pos" :20, "type" : "log"}, + "power_factorL3" : {"pos" :21, "type" : "log"}, + "frequency" : {"pos" :22, "type" : "log"}, + "active_energy" : {"pos" :23, "type" : "log"}, + "positiveActiveTodayEnergy" : {"pos" :23, "type" : "log"}, + "posiActivePower" : {"pos" :23, "type" : "log"}, + "reactive_energy" : {"pos" :24, "type" : "log"}, + "reverseActiveTodayEnergy" : {"pos" :24, "type" : "log"}, + "reverActivePower" : {"pos" :24, "type" : "log"}, + "end" : {"pos" :25, "type" : "log","incl" : "no"} + }, +"T06241b": { + "decrypt" : {"value" :"True"}, + "datalogserial" : {"value" :16, "length" : 10, "type" : "text","incl" : "yes"}, + "device" : {"value": "CHNT_THREE","type" : "def"}, + "date" : {"value" :80, "divide" : 10}, + "logstart" : {"value" :96,"type" : "def","incl" : "no"}, + "I1Voltage2" : {"pos" :1, "type" : "log"}, + "I2Voltage3" : {"pos" :2, "type" : "log"}, + "I3Voltage1" : {"pos" :3, "type" : "log"}, + "voltageL1" : {"pos" :4, "type" : "log"}, + "voltageL2" : {"pos" :5, "type" : "log"}, + "voltageL3" : {"pos" :6, "type" : "log"}, + "currentL1" : {"pos" :7, "type" : "log"}, + "currentL2" : {"pos" :8, "type" : "log"}, + "currentL3" : {"pos" :9, "type" : "log"}, + "ReverseActiveEnergy" : {"pos" :10, "type" : "log"}, + "activePowerL1" : {"pos" :11, "type" : "log"}, + "activePowerL2" : {"pos" :12, "type" : "log"}, + "activePowerL3" : {"pos" :13, "type" : "log"}, + "reactive_power" : {"pos" :14, "type" : "logneg"}, + "active_power" : {"pos" :14, "type" : "logpos"}, + "reactive_powerL1" : {"pos" :15, "type" : "log"}, + "reactive_powerL2" : {"pos" :16, "type" : "log"}, + "reactive_powerL3" : {"pos" :17, "type" : "log"}, + "power_factor" : {"pos" :18, "type" : "log"}, + "power_factorL1" : {"pos" :19, "type" : "log"}, + "power_factorL2" : {"pos" :20, "type" : "log"}, + "power_factorL3" : {"pos" :21, "type" : "log"}, + "frequency" : {"pos" :22, "type" : "log"}, + "active_energy" : {"pos" :23, "type" : "log"}, + "positiveActiveTodayEnergy" : {"pos" :23, "type" : "log"}, + "posiActivePower" : {"pos" :23, "type" : "log"}, + "reactive_energy" : {"pos" :24, "type" : "log"}, + "reverseActiveTodayEnergy" : {"pos" :24, "type" : "log"}, + "reverActivePower" : {"pos" :24, "type" : "log"}, + "end" : {"pos" :25, "type" : "log","incl" : "no"} + }} \ No newline at end of file From 3d91b05ef4280ebca6560df33c30dfa5c1ea1865 Mon Sep 17 00:00:00 2001 From: CyborgAndy Date: Sun, 25 May 2025 14:17:19 +0300 Subject: [PATCH 03/17] Update T06NNNNSPF layout - added Load Consumption values eopDischrToday and eopDischrTotal Signed-off-by: CyborgAndy --- grottconf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/grottconf.py b/grottconf.py index 78030916..62ce0d1f 100644 --- a/grottconf.py +++ b/grottconf.py @@ -1001,7 +1001,9 @@ def set_reclayouts(self): "BatDischarWatt" : {"value" :458, "length" : 4, "type" : "num", "divide" : 10}, "BatDischarVA" : {"value" :466, "length" : 4, "type" : "num", "divide" : 10}, "BatWatt" : {"value" :474, "length" : 4, "type" : "numx", "divide" : 10}, - "invfanspeed" : {"value" :494, "length" : 2, "type" : "num", "divide" : 1} + "invfanspeed" : {"value" :494, "length" : 2, "type" : "num", "divide" : 1}, + "eopDischrToday" : {"value" :506, "length" : 4, "type" : "num", "divide" : 10}, + "eopDischrTotal" : {"value" :514, "length" : 4, "type" : "num", "divide" : 10} }} self.recorddict9 = {"T06NNNNXTL3": { From 9aa4d7484e6344c1bde78f888ee969a55abf777a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Tue, 5 Aug 2025 20:21:43 +0200 Subject: [PATCH 04/17] Add files via upload --- grott_ha.py | 684 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 684 insertions(+) create mode 100644 grott_ha.py diff --git a/grott_ha.py b/grott_ha.py new file mode 100644 index 00000000..9186b9da --- /dev/null +++ b/grott_ha.py @@ -0,0 +1,684 @@ +# coding=utf-8 +# author Etienne G. + +import json +from datetime import datetime, timezone + +from paho.mqtt.publish import single, multiple + +from grottconf import Conf + +__version__ = "0.0.7-rc2" + +"""A pluging for grott +This plugin allow to have autodiscovery of the device in HA + +Should be able to support multiples inverters + +Config: + - ha_mqtt_host (required): The host of the MQTT broker user by HA (often the IP of HA) + - ha_mqtt_port (required): The port (the default is oftent 1883) + - ha_mqtt_user (optional): The user use to connect to the broker (you can use your user) + - ha_mqtt_password (optional): The password to connect to the mqtt broket (you can use your password) + +Return codes: + - 0: Everything is OK + - 1: Missing MQTT extvar configuration + - 2: Error while publishing the measure value message + - 3: MQTT connection error + - 4: Error while creating last_push status key + - 5: Refused to push a buffered message (prevent invalid stats, not en error) + - 6: Error while configuring HA MQTT sensor devices + - 7: Can't configure device for HA MQTT +""" + + +config_topic = "homeassistant/{sensor_type}/grott/{device}_{attribut}/config" +state_topic = "homeassistant/grott/{device}/state" + + +mapping = { + "datalogserial": { + "name": "Datalogger serial", + }, + "pvserial": {"name": "Serial"}, + "pv1watt": { + "name": "PV1 Watt", + "state_class": "measurement", + "device_class": "power", + "unit_of_measurement": "W", + }, + "pv1voltage": { + "name": "PV1 Voltage", + "state_class": "measurement", + "device_class": "voltage", + "unit_of_measurement": "V", + }, + "pv1current": { + "name": "PV1 Current", + "state_class": "measurement", + "device_class": "current", + "unit_of_measurement": "A", + }, + "pv2watt": { + "name": "PV2 Watt", + "state_class": "measurement", + "device_class": "power", + "unit_of_measurement": "W", + }, + "pv2voltage": { + "name": "PV2 Voltage", + "state_class": "measurement", + "device_class": "voltage", + "unit_of_measurement": "V", + }, + "pv2current": { + "name": "PV2 Current", + "state_class": "measurement", + "device_class": "current", + "unit_of_measurement": "A", + }, + "pvpowerin": { + "name": "PV Input (Actual)", + "state_class": "measurement", + "device_class": "power", + "unit_of_measurement": "W", + }, + "pvpowerout": { + "name": "PV Output (Actual)", + "state_class": "measurement", + "device_class": "power", + "unit_of_measurement": "W", + }, + "pvfrequentie": { + "name": "Grid frequency", + "state_class": "measurement", + "device_class": "frequency", + "unit_of_measurement": "Hz", + "icon": "mdi:waveform", + }, + # Grid config + "pvgridvoltage": { + "name": "Phase 1 voltage", + "state_class": "measurement", + "device_class": "voltage", + "unit_of_measurement": "V", + }, + "pvgridvoltage2": { + "name": "Phase 2 voltage", + "state_class": "measurement", + "device_class": "voltage", + "unit_of_measurement": "V", + }, + "pvgridvoltage3": { + "name": "Phase 3 voltage", + "state_class": "measurement", + "device_class": "voltage", + "unit_of_measurement": "V", + }, + "pvgridcurrent": { + "name": "Phase 1 current", + "state_class": "measurement", + "device_class": "current", + "unit_of_measurement": "A", + }, + "pvgridcurrent2": { + "state_class": "measurement", + "device_class": "current", + "name": "Phase 2 current", + "unit_of_measurement": "A", + }, + "pvgridcurrent3": { + "name": "Phase 3 current", + "state_class": "measurement", + "device_class": "current", + "unit_of_measurement": "A", + }, + "pvgridpower": { + "name": "Phase 1 power", + "state_class": "measurement", + "device_class": "power", + "unit_of_measurement": "W", + }, + "pvgridpower2": { + "name": "Phase 2 power", + "state_class": "measurement", + "device_class": "power", + "unit_of_measurement": "W", + }, + "pvgridpower3": { + "name": "Phase 3 power", + "state_class": "measurement", + "device_class": "power", + "unit_of_measurement": "W", + }, + # End grid + "pvenergytoday": { + "name": "Generated energy (Today)", + "state_class": "total", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:solar-power", + }, + "epvtoday": { + "name": "PV Energy today (Today)", + "state_class": "total", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:solar-power", + }, + "epv1today": { + "name": "Solar PV1 production", + "state_class": "total", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:solar-power", + }, + "epv2today": { + "name": "Solar PV2 production", + "state_class": "total", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:solar-power", + }, + "pvenergytotal": { + "state_class": "total_increasing", + "device_class": "energy", + "name": "Generated energy (Total)", + "unit_of_measurement": "kWh", + "icon": "mdi:solar-power", + }, + "epvtotal": { + "name": "Generated PV energy (Total)", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:solar-power", + "state_class": "total", + }, + "epv1total": { + "name": "Solar PV1 production (Total)", + "state_class": "total", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:solar-power", + }, + "epv2total": { + "name": "Solar PV2 production (Total)", + "state_class": "total", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:solar-power", + }, + # For SPH compatiblity + "epvTotal": { + "name": "Generated PV energy (Total)", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:solar-power", + "state_class": "total", + }, + "pactogridr": { + "name": "Energy export (Today)", + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "Wh", + "state_class": "total", + "icon": "mdi:solar-power", + }, + "pactogridtot": { + "name": "Energy export (Total)", + "device_class": "energy", + "state_class": "measurement", + "unit_of_measurement": "Wh", + "state_class": "total_increasing", + "icon": "mdi:solar-power", + }, + "pvstatus": { + "name": "State", + # "value_template": "{% if value_json.pvstatus == 0 %}Standby{% elif value_json.pvstatus == 1 %}Normal{% elif value_json.pvstatus == 2 %}Fault{% else %}Unknown{% endif %}", + "icon": "mdi:power-settings", + }, + "totworktime": { + "name": "Working time", + "device_class": "duration", + "unit_of_measurement": "hours", + "value_template": "{{ value_json.totworktime| float / 7200 | round(2) }}", + }, + "pvtemperature": { + "name": "Inverter temperature", + "state_class": "measurement", + "device_class": "temperature", + "unit_of_measurement": "°C", + }, + "pvipmtemperature": { + "name": "IPM temperature", + "device_class": "temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + }, + "pvboottemperature": { + "name": "Inverter boost temperature", + "device_class": "temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + }, + "pvboosttemp": { + "name": "Inverter boost temperature", + "device_class": "temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + }, + "etogrid_tod": { + "name": "Energy to grid (Today)", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:transmission-tower-import", + "state_class": "total", + }, + "etogrid_tot": { + "name": "Energy to grid (Total)", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:transmission-tower-import", + "state_class": "total_increasing", + }, + "etouser_tod": { + "name": "Import from grid (Today)", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:solar-power", + "state_class": "total", + }, + "etouser_tot": { + "name": "Import from grid (Total)", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:transmission-tower-export", + "state_class": "total_increasing", + }, + "pactouserr": { + "name": "Import from grid (Actual)", + "device_class": "energy", + "device_class": "power", + "unit_of_measurement": "W", + "icon": "mdi:transmission-tower-export", + }, + # Register 1015 # TODO: investiagate + # "pactousertot": { + # "name": "Power consumption total", + # "device_class": "power", + # "unit_of_measurement": "kW", + # "icon": "mdi:transmission-tower-export", + # }, + "elocalload_tod": { + "name": "Load consumption (Today)", + "device_class": "energy", + "unit_of_measurement": "Wh", + "icon": "mdi:solar-power", + "state_class": "total", + }, + "elocalload_tot": { + "name": "Load consumption (Total)", + "device_class": "energy", + "unit_of_measurement": "Wh", + "icon": "mdi:solar-power", + "state_class": "total_increasing", + }, + "plocaloadr": { + "name": "Local load consumption", + "device_class": "power", + "unit_of_measurement": "W", + "icon": "mdi:transmission-tower-export", + }, + "grott_last_push": { + "name": "Grott last data push", + "device_class": "timestamp", + "value_template": "{{value_json.grott_last_push}}", + }, + "grott_last_measure": { + "name": "Last measure", + "device_class": "timestamp", + }, + # batteries + "eacharge_today": { + "name": "Battery charge from AC (Today)", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:battery-arrow-up", + "state_class": "total", + }, + "eacharge_total": { + "name": "Battery charge from AC (Today)", + "device_class": "energy", + "unit_of_measurement": "kWh", + "icon": "mdi:solar-power", + "state_class": "total_increasing", + }, + "vbat": { + "name": "Battery voltage", + "state_class": "measurement", + "device_class": "voltage", + "unit_of_measurement": "V", + }, + "SOC": { + "name": "Battery charge", + "device_class": "battery", + "state_class": "measurement", + "unit_of_measurement": "%", + "icon": "mdi:battery-charging-60", + }, + # taken from register 1048 of RTU manual v1.20 + "batterytype": { + "name": "Batteries type", + "value_template": "{% if value_json.batterytype == 0 %}Lithium{% elif value_json.batterytype == '1' %}Lead-acid{% elif value_json.batterytype == '2' %}Other{% else %}Unknown{% endif %}", + "icon": "mdi:power-settings", + }, + "p1charge1": { + "name": "Battery charge", + "device_class": "power", + "unit_of_measurement": "kW", + "state_class": "measurement", + "icon": "mdi:battery-arrow-up", + }, + "eharge1_tod": { + "name": "Battery charge (Today)", + "device_class": "energy", + "state_class": "total", + "unit_of_measurement": "kWh", + "icon": "mdi:battery-arrow-up", + }, + "eharge1_tot": { + "name": "Battery charge (Total)", + "device_class": "energy", + "state_class": "total_increasing", + "unit_of_measurement": "kWh", + "icon": "mdi:battery-arrow-up", + }, + "edischarge1_tod": { + "name": "Battery discharge (Today)", + "device_class": "energy", + "state_class": "total", + "unit_of_measurement": "kWh", + "icon": "mdi:battery-arrow-down", + }, + "edischarge1_tot": { + "name": "Battery discharge (Total)", + "device_class": "energy", + "state_class": "total_increasing", + "unit_of_measurement": "kWh", + "icon": "mdi:battery-arrow-down", + }, + "battemp": { + "name": "Battery temperature", + "device_class": "temperature", + "unit_of_measurement": "°C", + "icon": "mdi:thermometer", + }, + "spbusvolt": { + "state_class": "measurement", + "device_class": "voltage", + "name": "BP bus voltage", + "unit_of_measurement": "V", + }, + "systemfaultword1": { + "name": "System fault register 1", + }, + "systemfaultword2": { + "name": "System fault register 2", + }, + "systemfaultword3": { + "name": "System fault register 3", + }, + "systemfaultword4": { + "name": "System fault register 4", + }, + "systemfaultword5": { + "name": "System fault register 5", + }, + "systemfaultword6": { + "name": "System fault register 6", + }, + "systemfaultword7": { + "name": "System fault register 7", + }, + "vpv1": { + "name": "PV1 Voltage", + "state_class": "measurement", + "device_class": "voltage", + "unit_of_measurement": "V", + }, + "vpv2": { + "name": "PV2 Voltage", + "state_class": "measurement", + "device_class": "voltage", + "unit_of_measurement": "V", + }, + "ppv1": { + "name": "PV1 charge power", + "device_class": "power", + "unit_of_measurement": "W", + "state_class": "measurement", + }, + "ppv2": { + "name": "PV1 charge power", + "device_class": "power", + "unit_of_measurement": "W", + "state_class": "measurement", + }, + "buck1curr": { + "name": "Buck1 current", + "device_class": "current", + "unit_of_measurement": "A", + "state_class": "measurement", + }, + "buck2curr": { + "name": "Buck2 current", + "device_class": "current", + "unit_of_measurement": "A", + "state_class": "measurement", + }, + "op_watt": { + "name": "Output active power", + "device_class": "power", + "unit_of_measurement": "W", + "state_class": "measurement", + }, + "op_va": { + "name": "Output apparent power", + "device_class": "apparent_power", + "unit_of_measurement": "VA", + "state_class": "measurement", + }, +} + + +def make_payload(conf: Conf, device: str, name: str, key: str, unit: str = None): + # Default configuration payload + payload = { + "name": "{name}", + "unique_id": f"grott_{device}_{key}", # Generate a unique device ID + "state_topic": f"homeassistant/grott/{device}/state", + "device": { + "identifiers": [device], # Group under a device + "name": device, + "manufacturer": "GroWatt", + }, + } + + # If there's a custom mapping add the new values + if key in mapping: + payload.update(mapping[key]) + + # Reuse the existing divide value if available and not existing + # and apply it to the HA config + layout = conf.recorddict[conf.layout] + if "value_template" not in payload and key in layout: + # From grottdata:207, default type is num, also process numx + if layout[key].get("type", "num") in ("num", "numx") and layout[key].get( + "divide", "1" + ): + payload[ + "value_template" + ] = "{{{{ value_json.{key} | float / {divide} }}}}".format( + key=key, + divide=layout[key].get("divide"), + ) + + if "value_template" not in payload: + payload["value_template"] = f"{{{{ value_json.{key} }}}}" + + return payload + + +class MqttStateHandler: + __pv_config = {} + client_name = "Grott - HA" + + @classmethod + def is_configured(cls, serial: str): + return cls.__pv_config.get(serial, False) + + @classmethod + def set_configured(cls, serial: str): + cls.__pv_config[serial] = True + + +def process_conf(conf: Conf): + required_params = [ + "ha_mqtt_host", + "ha_mqtt_port", + ] + if not all([param in conf.extvar for param in required_params]): + print("Missing configuration for ha_mqtt") + raise AttributeError + + if "ha_mqtt_user" in conf.extvar: + auth = { + "username": conf.extvar["ha_mqtt_user"], + "password": conf.extvar["ha_mqtt_password"], + } + else: + auth = None + + # Need to convert the port if passed as a string + port = conf.extvar["ha_mqtt_port"] + if isinstance(port, str): + port = int(port) + return { + "client_id": MqttStateHandler.client_name, + "auth": auth, + "hostname": conf.extvar["ha_mqtt_host"], + "port": port, + } + + +def publish_single(conf: Conf, topic, payload, retain=False): + conf = process_conf(conf) + return single(topic, payload=payload, retain=retain, **conf) + + +def publish_multiple(conf: Conf, msgs): + conf = process_conf(conf) + return multiple(msgs, **conf) + + +def grottext(conf: Conf, data: str, jsonmsg: str): + """Allow to push to HA MQTT bus, with auto discovery""" + + required_params = [ + "ha_mqtt_host", + "ha_mqtt_port", + ] + if not all([param in conf.extvar for param in required_params]): + print("Missing configuration for ha_mqtt") + return 1 + + # Need to decode the json string + jsonmsg = json.loads(jsonmsg) + + if jsonmsg.get("buffered") == "yes": + # Skip buffered message, HA don't support them + if conf.verbose: + print("\t - Grott HA - skipped buffered") + return 5 + + device_serial = jsonmsg["device"] + values = jsonmsg["values"] + + # Send the last push in UTC with TZ + dt = datetime.now(timezone.utc) + # Add a new value to the existing values + values["grott_last_push"] = dt.isoformat() + + # Layout can be undefined + if not MqttStateHandler.is_configured(device_serial) and getattr( + conf, "layout", None + ): + configs_payloads = [] + print(f"\tGrott HA {__version__} - creating {device_serial} config in HA") + for key in values.keys(): + # Generate a configuration payload + payload = make_payload(conf, device_serial, key, key) + if not payload: + print(f"\t[Grott HA] {__version__} skipped key: {key}") + continue + + try: + topic = config_topic.format( + sensor_type="sensor", + device=device_serial, + attribut=key, + ) + configs_payloads.append( + { + "topic": topic, + "payload": json.dumps(payload), + "retain": True, + } + ) + except Exception as e: + print( + f"\t - [grott HA] {__version__} Exception while creating new sensor {key}: {e}" + ) + return 6 + + # Create a virtual last_push key to allow tracking when there was the last data transmission + + try: + key = "grott_last_push" + payload = make_payload(conf, device_serial, key, key) + topic = config_topic.format( + sensor_type="sensor", + device=device_serial, + attribut=key, + ) + configs_payloads.append( + { + "topic": topic, + "payload": json.dumps(payload), + "retain": True, + } + ) + except Exception as e: + print( + f"\t - [grott HA] {__version__} Exception while creating new sensor last push: {e}" + ) + return 4 + publish_multiple(conf, configs_payloads) + # Now it's configured, no need to come back + MqttStateHandler.set_configured(device_serial) + + if not MqttStateHandler.is_configured(device_serial): + print(f"\t[Grott HA] {__version__} Can't configure device: {device_serial}") + return 7 + + # Push the vales to the topics + try: + publish_single( + conf, state_topic.format(device=device_serial), json.dumps(values) + ) + except Exception as e: + print("[HA ext] - Exception while publishing - {}".format(e)) + # Reset connection state in case of problem + return 2 + return 0 From 322dba53df7bf23706229eb08d0903fa6d313724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Tue, 5 Aug 2025 20:33:21 +0200 Subject: [PATCH 05/17] Create first ghcr.io image --- .github/workflows/docker-image.yml | 23 +++++++++++++++++++++++ docker/dockerfile | 3 ++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docker-image.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 00000000..84da8d7a --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,23 @@ +name: Docker Image CI + +on: + push: + branches: [ "master","alpha" ] + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Log in to registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + - name: Get Short SHA + id: get_short_sha + run: echo "SHORT_SHA=$(echo $GITHUB_SHA | cut -c1-7)" >> $GITHUB_ENV + - name: Build and push the Docker image + run: | + docker build . --file docker/dockerfile --tag grott:$SHORT_SHA + docker push ghcr.io/${{ github.actor }}/grott:$SHORT_SHA diff --git a/docker/dockerfile b/docker/dockerfile index b082f9e3..b8328792 100644 --- a/docker/dockerfile +++ b/docker/dockerfile @@ -20,7 +20,8 @@ COPY grottconf.py /app/grottconf.py COPY grottdata.py /app/grottdata.py COPY grottproxy.py /app/grottproxy.py COPY grottsniffer.py /app/grottsniffer.py -COPY grott.ini /app/grott.ini +# COPY grott.ini /app/grott.ini +COPY grott_ha.py /app/grott_ha.py WORKDIR /app CMD ["python", "-u", "grott.py", "-v"] \ No newline at end of file From 557960b5cb8c21dd54507c0bf051ce9fac13c118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Tue, 5 Aug 2025 20:39:55 +0200 Subject: [PATCH 06/17] Update docker-image.yml --- .github/workflows/docker-image.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 84da8d7a..5f7c1d7a 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -19,5 +19,6 @@ jobs: run: echo "SHORT_SHA=$(echo $GITHUB_SHA | cut -c1-7)" >> $GITHUB_ENV - name: Build and push the Docker image run: | - docker build . --file docker/dockerfile --tag grott:$SHORT_SHA + docker build . --file docker/dockerfile --tag ghcr.io/${{ github.actor }}/grott:$SHORT_SHA docker push ghcr.io/${{ github.actor }}/grott:$SHORT_SHA + From 5753b24a4b49b74ace0f5ce578bf6d5f830be49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Tue, 5 Aug 2025 20:44:23 +0200 Subject: [PATCH 07/17] Update grottconf.py --- grottconf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grottconf.py b/grottconf.py index 751b2750..bb447ed6 100644 --- a/grottconf.py +++ b/grottconf.py @@ -1722,8 +1722,7 @@ def set_reclayouts(self): "bdc2_ibat" : {"value" : 938,"length" : 2,"type" : "num","divide" : 10}, "bdc2_soc" : {"value" : 942,"length" : 2,"type" : "num","divide" : 1}, "bdc2_vbus1" : {"value" : 946,"length" : 2,"type" : "num","divide" : 10}, - "bdc2_vbus2" : {"value" : 950,"length" : 2,"type" : " - num","divide" : 10}, + "bdc2_vbus2" : {"value" : 950,"length" : 2,"type" : "num","divide" : 10}, "bdc2_ibb" : {"value" : 954,"length" : 2,"type" : "num","divide" : 10}, "bdc2_illc" : {"value" : 958,"length" : 2,"type" : "num","divide" : 10}, "bdc2_tempa" : {"value" : 962,"length" : 2,"type" : "num","divide" : 10}, @@ -2327,3 +2326,4 @@ def set_reclayouts(self): print(logger.level) logger.info("\t{0}".format(key)) logger.debugv("\n{0}\n".format(format_multi_line("\t", str(self.alodict[key]),120))) + From 1e3d64133f4b4dc869f88e84d11f7e54c8d2e5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Tue, 5 Aug 2025 20:47:02 +0200 Subject: [PATCH 08/17] Update dockerfile --- docker/dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/dockerfile b/docker/dockerfile index b8328792..36be2177 100644 --- a/docker/dockerfile +++ b/docker/dockerfile @@ -20,8 +20,9 @@ COPY grottconf.py /app/grottconf.py COPY grottdata.py /app/grottdata.py COPY grottproxy.py /app/grottproxy.py COPY grottsniffer.py /app/grottsniffer.py +COPY grottserver.py /app/grottserver.py # COPY grott.ini /app/grott.ini COPY grott_ha.py /app/grott_ha.py WORKDIR /app -CMD ["python", "-u", "grott.py", "-v"] \ No newline at end of file +CMD ["python", "-u", "grott.py", "-v"] From badb1164b57113fedbcfcc08ff8a176332bcd298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Tue, 5 Aug 2025 20:50:20 +0200 Subject: [PATCH 09/17] Update dockerfile --- docker/dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/dockerfile b/docker/dockerfile index 36be2177..04faa2a3 100644 --- a/docker/dockerfile +++ b/docker/dockerfile @@ -14,6 +14,7 @@ RUN pip install paho-mqtt RUN pip install requests RUN pip install influxdb RUN pip install influxdb-client +RUN pip install psutil COPY grott.py /app/grott.py COPY grottconf.py /app/grottconf.py From 67bc31d35b46efe6bfcf1b5a80a6b4e92fadaa44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Fri, 8 May 2026 08:00:24 +0200 Subject: [PATCH 10/17] Rename client_name from 'Grott - HA' to 'grott' because it was rejected as invalid by emqx --- grott_ha.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grott_ha.py b/grott_ha.py index 9186b9da..c8d884d2 100644 --- a/grott_ha.py +++ b/grott_ha.py @@ -532,7 +532,7 @@ def make_payload(conf: Conf, device: str, name: str, key: str, unit: str = None) class MqttStateHandler: __pv_config = {} - client_name = "Grott - HA" + client_name = "grott" @classmethod def is_configured(cls, serial: str): From d49193a94171d5a28822f4121b708dae29a4579f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Fri, 8 May 2026 08:37:09 +0200 Subject: [PATCH 11/17] Log configuration when publishing single and multiple messages Added print statements to log publishing configuration. --- grott_ha.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/grott_ha.py b/grott_ha.py index c8d884d2..0ef94969 100644 --- a/grott_ha.py +++ b/grott_ha.py @@ -574,11 +574,13 @@ def process_conf(conf: Conf): def publish_single(conf: Conf, topic, payload, retain=False): conf = process_conf(conf) + print("publishing single with", conf) return single(topic, payload=payload, retain=retain, **conf) def publish_multiple(conf: Conf, msgs): conf = process_conf(conf) + print("publishing multiple with", conf) return multiple(msgs, **conf) From c2431d6c6201ec05b19b5f4ef9efed4238137e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Fri, 8 May 2026 11:39:12 +0200 Subject: [PATCH 12/17] Attempt with persistent client. --- grott_ha.py | 103 ++++++++++++++++++++++------------------------------ 1 file changed, 44 insertions(+), 59 deletions(-) diff --git a/grott_ha.py b/grott_ha.py index 0ef94969..0c45c6c4 100644 --- a/grott_ha.py +++ b/grott_ha.py @@ -4,7 +4,8 @@ import json from datetime import datetime, timezone -from paho.mqtt.publish import single, multiple +import paho.mqtt.client as mqtt +import time from grottconf import Conf @@ -532,7 +533,41 @@ def make_payload(conf: Conf, device: str, name: str, key: str, unit: str = None) class MqttStateHandler: __pv_config = {} - client_name = "grott" + __conf = None + + client = mqtt.Client(client_id="grott") + connected = False + def on_connect(client, userdata, flags, reason_code, properties): + print(f"Connected with result code {reason_code}") + connected = reason_code == 0 + client.on_connect = on_connect + + def on_disconnect(client, userdata, rc): + logging.info("disconnecting reason " +str(rc)) + connected = False + + @classmethod + def connect(conf: Conf): + __conf = conf + required_params = [ + "ha_mqtt_host", + "ha_mqtt_port", + ] + if not all([param in conf.extvar for param in required_params]): + print("Missing configuration for ha_mqtt") + raise AttributeError + + if "ha_mqtt_user" in conf.extvar: + client.username_pw_set(conf.extvar["ha_mqtt_user"], conf.extvar["ha_mqtt_password"]) + + port = conf.extvar["ha_mqtt_port"] + if isinstance(port, str): + port = int(port) + client.loop_start() + client.connect(conf.extvar["ha_mqtt_host"], port) + while (not connected): + time.sleep(1) + @classmethod def is_configured(cls, serial: str): @@ -542,46 +577,8 @@ def is_configured(cls, serial: str): def set_configured(cls, serial: str): cls.__pv_config[serial] = True - -def process_conf(conf: Conf): - required_params = [ - "ha_mqtt_host", - "ha_mqtt_port", - ] - if not all([param in conf.extvar for param in required_params]): - print("Missing configuration for ha_mqtt") - raise AttributeError - - if "ha_mqtt_user" in conf.extvar: - auth = { - "username": conf.extvar["ha_mqtt_user"], - "password": conf.extvar["ha_mqtt_password"], - } - else: - auth = None - - # Need to convert the port if passed as a string - port = conf.extvar["ha_mqtt_port"] - if isinstance(port, str): - port = int(port) - return { - "client_id": MqttStateHandler.client_name, - "auth": auth, - "hostname": conf.extvar["ha_mqtt_host"], - "port": port, - } - - -def publish_single(conf: Conf, topic, payload, retain=False): - conf = process_conf(conf) - print("publishing single with", conf) - return single(topic, payload=payload, retain=retain, **conf) - - -def publish_multiple(conf: Conf, msgs): - conf = process_conf(conf) - print("publishing multiple with", conf) - return multiple(msgs, **conf) +def publish(topic, payload, retain=False): + MqttStateHandler.client.publish(topic, payload=payload, retain=retain) def grottext(conf: Conf, data: str, jsonmsg: str): @@ -616,7 +613,8 @@ def grottext(conf: Conf, data: str, jsonmsg: str): if not MqttStateHandler.is_configured(device_serial) and getattr( conf, "layout", None ): - configs_payloads = [] + print(f"\tGrott HA {__version__} - Connecting MQTT") + MqttStateHandler.connect(conf) print(f"\tGrott HA {__version__} - creating {device_serial} config in HA") for key in values.keys(): # Generate a configuration payload @@ -631,13 +629,7 @@ def grottext(conf: Conf, data: str, jsonmsg: str): device=device_serial, attribut=key, ) - configs_payloads.append( - { - "topic": topic, - "payload": json.dumps(payload), - "retain": True, - } - ) + publish(topic, json.dumps(payload), True) except Exception as e: print( f"\t - [grott HA] {__version__} Exception while creating new sensor {key}: {e}" @@ -654,19 +646,12 @@ def grottext(conf: Conf, data: str, jsonmsg: str): device=device_serial, attribut=key, ) - configs_payloads.append( - { - "topic": topic, - "payload": json.dumps(payload), - "retain": True, - } - ) + publish(topic, json.dumps(payload), True) except Exception as e: print( f"\t - [grott HA] {__version__} Exception while creating new sensor last push: {e}" ) return 4 - publish_multiple(conf, configs_payloads) # Now it's configured, no need to come back MqttStateHandler.set_configured(device_serial) @@ -676,7 +661,7 @@ def grottext(conf: Conf, data: str, jsonmsg: str): # Push the vales to the topics try: - publish_single( + publish( conf, state_topic.format(device=device_serial), json.dumps(values) ) except Exception as e: From 7c83e4948330eee0a32a1329b37695890c997f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Fri, 8 May 2026 11:45:23 +0200 Subject: [PATCH 13/17] Minor bugfix on class method --- grott_ha.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/grott_ha.py b/grott_ha.py index 0c45c6c4..80c389b2 100644 --- a/grott_ha.py +++ b/grott_ha.py @@ -533,7 +533,6 @@ def make_payload(conf: Conf, device: str, name: str, key: str, unit: str = None) class MqttStateHandler: __pv_config = {} - __conf = None client = mqtt.Client(client_id="grott") connected = False @@ -547,8 +546,7 @@ def on_disconnect(client, userdata, rc): connected = False @classmethod - def connect(conf: Conf): - __conf = conf + def connect(cls, conf: Conf): required_params = [ "ha_mqtt_host", "ha_mqtt_port", @@ -558,14 +556,14 @@ def connect(conf: Conf): raise AttributeError if "ha_mqtt_user" in conf.extvar: - client.username_pw_set(conf.extvar["ha_mqtt_user"], conf.extvar["ha_mqtt_password"]) + cls.client.username_pw_set(conf.extvar["ha_mqtt_user"], conf.extvar["ha_mqtt_password"]) port = conf.extvar["ha_mqtt_port"] if isinstance(port, str): port = int(port) - client.loop_start() - client.connect(conf.extvar["ha_mqtt_host"], port) - while (not connected): + cls.client.loop_start() + cls.client.connect(conf.extvar["ha_mqtt_host"], port) + while (not cls.connected): time.sleep(1) From 08b655f21e422659d8e8054e741f19827888a5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Fri, 8 May 2026 11:52:17 +0200 Subject: [PATCH 14/17] Something with api callback versions --- grott_ha.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/grott_ha.py b/grott_ha.py index 80c389b2..87ee6bc2 100644 --- a/grott_ha.py +++ b/grott_ha.py @@ -536,13 +536,13 @@ class MqttStateHandler: client = mqtt.Client(client_id="grott") connected = False - def on_connect(client, userdata, flags, reason_code, properties): + def on_connect(client, userdata, flags, reason_code): print(f"Connected with result code {reason_code}") connected = reason_code == 0 client.on_connect = on_connect - def on_disconnect(client, userdata, rc): - logging.info("disconnecting reason " +str(rc)) + def on_disconnect(client, userdata, reason_code): + logging.info("disconnecting reason " +str(reason_code)) connected = False @classmethod From d6246e0e0d613e3334155a112fe596585ab813b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Fri, 8 May 2026 12:01:40 +0200 Subject: [PATCH 15/17] Moved variable into client connected because otherwise it would make a new local vairable --- grott_ha.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/grott_ha.py b/grott_ha.py index 87ee6bc2..6804d6cb 100644 --- a/grott_ha.py +++ b/grott_ha.py @@ -535,15 +535,15 @@ class MqttStateHandler: __pv_config = {} client = mqtt.Client(client_id="grott") - connected = False + client.connected = False def on_connect(client, userdata, flags, reason_code): print(f"Connected with result code {reason_code}") - connected = reason_code == 0 + client.connected = reason_code == 0 client.on_connect = on_connect def on_disconnect(client, userdata, reason_code): logging.info("disconnecting reason " +str(reason_code)) - connected = False + client.connected = False @classmethod def connect(cls, conf: Conf): @@ -563,8 +563,9 @@ def connect(cls, conf: Conf): port = int(port) cls.client.loop_start() cls.client.connect(conf.extvar["ha_mqtt_host"], port) - while (not cls.connected): + while (not cls.client.connected): time.sleep(1) + print("MQTT Connected!") @classmethod From 5d095ffb8762d179983f3b28e9cbc25ae51ecd71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Fri, 8 May 2026 12:04:26 +0200 Subject: [PATCH 16/17] Fixed one class still using old publish syntax --- grott_ha.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grott_ha.py b/grott_ha.py index 6804d6cb..0e4a5870 100644 --- a/grott_ha.py +++ b/grott_ha.py @@ -661,7 +661,7 @@ def grottext(conf: Conf, data: str, jsonmsg: str): # Push the vales to the topics try: publish( - conf, state_topic.format(device=device_serial), json.dumps(values) + state_topic.format(device=device_serial), json.dumps(values) ) except Exception as e: print("[HA ext] - Exception while publishing - {}".format(e)) From ac7bc0fd961ec2378ee7699dc18bc75e679845cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Fri, 8 May 2026 12:14:37 +0200 Subject: [PATCH 17/17] Undid client change, changed client id in grottdata. --- grott_ha.py | 103 +++++++++++++++++++++++++++++---------------------- grottdata.py | 2 +- 2 files changed, 60 insertions(+), 45 deletions(-) diff --git a/grott_ha.py b/grott_ha.py index 0e4a5870..72b1ffc8 100644 --- a/grott_ha.py +++ b/grott_ha.py @@ -4,8 +4,7 @@ import json from datetime import datetime, timezone -import paho.mqtt.client as mqtt -import time +from paho.mqtt.publish import single, multiple from grottconf import Conf @@ -533,40 +532,8 @@ def make_payload(conf: Conf, device: str, name: str, key: str, unit: str = None) class MqttStateHandler: __pv_config = {} - - client = mqtt.Client(client_id="grott") - client.connected = False - def on_connect(client, userdata, flags, reason_code): - print(f"Connected with result code {reason_code}") - client.connected = reason_code == 0 - client.on_connect = on_connect - - def on_disconnect(client, userdata, reason_code): - logging.info("disconnecting reason " +str(reason_code)) - client.connected = False - - @classmethod - def connect(cls, conf: Conf): - required_params = [ - "ha_mqtt_host", - "ha_mqtt_port", - ] - if not all([param in conf.extvar for param in required_params]): - print("Missing configuration for ha_mqtt") - raise AttributeError - - if "ha_mqtt_user" in conf.extvar: - cls.client.username_pw_set(conf.extvar["ha_mqtt_user"], conf.extvar["ha_mqtt_password"]) - - port = conf.extvar["ha_mqtt_port"] - if isinstance(port, str): - port = int(port) - cls.client.loop_start() - cls.client.connect(conf.extvar["ha_mqtt_host"], port) - while (not cls.client.connected): - time.sleep(1) - print("MQTT Connected!") - + # client_name = "Grott - HA" + client_name = "grott" @classmethod def is_configured(cls, serial: str): @@ -576,8 +543,44 @@ def is_configured(cls, serial: str): def set_configured(cls, serial: str): cls.__pv_config[serial] = True -def publish(topic, payload, retain=False): - MqttStateHandler.client.publish(topic, payload=payload, retain=retain) + +def process_conf(conf: Conf): + required_params = [ + "ha_mqtt_host", + "ha_mqtt_port", + ] + if not all([param in conf.extvar for param in required_params]): + print("Missing configuration for ha_mqtt") + raise AttributeError + + if "ha_mqtt_user" in conf.extvar: + auth = { + "username": conf.extvar["ha_mqtt_user"], + "password": conf.extvar["ha_mqtt_password"], + } + else: + auth = None + + # Need to convert the port if passed as a string + port = conf.extvar["ha_mqtt_port"] + if isinstance(port, str): + port = int(port) + return { + "client_id": MqttStateHandler.client_name, + "auth": auth, + "hostname": conf.extvar["ha_mqtt_host"], + "port": port, + } + + +def publish_single(conf: Conf, topic, payload, retain=False): + conf = process_conf(conf) + return single(topic, payload=payload, retain=retain, **conf) + + +def publish_multiple(conf: Conf, msgs): + conf = process_conf(conf) + return multiple(msgs, **conf) def grottext(conf: Conf, data: str, jsonmsg: str): @@ -612,8 +615,7 @@ def grottext(conf: Conf, data: str, jsonmsg: str): if not MqttStateHandler.is_configured(device_serial) and getattr( conf, "layout", None ): - print(f"\tGrott HA {__version__} - Connecting MQTT") - MqttStateHandler.connect(conf) + configs_payloads = [] print(f"\tGrott HA {__version__} - creating {device_serial} config in HA") for key in values.keys(): # Generate a configuration payload @@ -628,7 +630,13 @@ def grottext(conf: Conf, data: str, jsonmsg: str): device=device_serial, attribut=key, ) - publish(topic, json.dumps(payload), True) + configs_payloads.append( + { + "topic": topic, + "payload": json.dumps(payload), + "retain": True, + } + ) except Exception as e: print( f"\t - [grott HA] {__version__} Exception while creating new sensor {key}: {e}" @@ -645,12 +653,19 @@ def grottext(conf: Conf, data: str, jsonmsg: str): device=device_serial, attribut=key, ) - publish(topic, json.dumps(payload), True) + configs_payloads.append( + { + "topic": topic, + "payload": json.dumps(payload), + "retain": True, + } + ) except Exception as e: print( f"\t - [grott HA] {__version__} Exception while creating new sensor last push: {e}" ) return 4 + publish_multiple(conf, configs_payloads) # Now it's configured, no need to come back MqttStateHandler.set_configured(device_serial) @@ -660,8 +675,8 @@ def grottext(conf: Conf, data: str, jsonmsg: str): # Push the vales to the topics try: - publish( - state_topic.format(device=device_serial), json.dumps(values) + publish_single( + conf, state_topic.format(device=device_serial), json.dumps(values) ) except Exception as e: print("[HA ext] - Exception while publishing - {}".format(e)) diff --git a/grottdata.py b/grottdata.py index 35ca2bf5..472bc810 100644 --- a/grottdata.py +++ b/grottdata.py @@ -634,7 +634,7 @@ def procdata(conf,data): mqtt_publisher = MQTTPublisher( hostname=conf.mqttip, port=conf.mqttport, - client_id=conf.inverterid, + client_id="grott",#conf.inverterid, auth=conf.pubauth, socket_timeout=1.0 )