diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 00000000..5f7c1d7a --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,24 @@ +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 ghcr.io/${{ github.actor }}/grott:$SHORT_SHA + docker push ghcr.io/${{ github.actor }}/grott:$SHORT_SHA + diff --git a/docker/dockerfile b/docker/dockerfile index b082f9e3..04faa2a3 100644 --- a/docker/dockerfile +++ b/docker/dockerfile @@ -14,13 +14,16 @@ 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 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 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"] 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 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 diff --git a/grott_ha.py b/grott_ha.py new file mode 100644 index 00000000..72b1ffc8 --- /dev/null +++ b/grott_ha.py @@ -0,0 +1,685 @@ +# 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" + client_name = "grott" + + @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 diff --git a/grottconf.py b/grottconf.py index 255f98fc..bb447ed6 100644 --- a/grottconf.py +++ b/grottconf.py @@ -1154,34 +1154,36 @@ def set_reclayouts(self): "outputfreq" : {"value" :250, "length" : 2, "type" : "num", "divide" : 100}, "invtemp" : {"value" :258, "length" : 2, "type" : "num", "divide" : 10}, "dcdctemp" : {"value" :262, "length" : 2, "type" : "num", "divide" : 10}, - "loadpercent" : {"value" :266, "length" : 2, "type" : "num", "divide" : 10}, - "buck1_ntc" : {"value" :286, "length" : 2, "type" : "num", "divide" : 10}, - "buck2_ntc" : {"value" :290, "length" : 2, "type" : "num", "divide" : 10}, - "OP_Curr" : {"value" :294, "length" : 2, "type" : "num", "divide" : 10}, - "Inv_Curr" : {"value" :298, "length" : 2, "type" : "num", "divide" : 10}, - "AC_InWatt" : {"value" :302, "length" : 4, "type" : "num", "divide" : 10}, - "AC_InVA" : {"value" :310, "length" : 4, "type" : "num", "divide" : 10}, - "faultBit" : {"value" :318, "length" : 2, "type" : "num", "divide" : 1}, - "warningBit" : {"value" :322, "length" : 2, "type" : "num", "divide" : 1}, - "faultValue" : {"value" :326, "length" : 2, "type" : "num", "divide" : 1}, - "warningValue" : {"value" :330, "length" : 2, "type" : "num", "divide" : 1}, - "constantPowerOK" : {"value" :346, "length" : 2, "type" : "num", "divide" : 1}, - "epvtoday" : {"value" :358, "length" : 4, "type" : "num", "divide" : 10}, - "pvenergytoday" : {"value" :358, "length" : 4, "type" : "num", "divide" : 10}, - "epvtotal" : {"value" :366, "length" : 4, "type" : "num", "divide" : 10}, - "eacCharToday" : {"value" :390, "length" : 4, "type" : "num", "divide" : 10}, - "eacCharTotal" : {"value" :398, "length" : 4, "type" : "num", "divide" : 10}, - "ebatDischarToday" : {"value" :406, "length" : 4, "type" : "num", "divide" : 10}, - "ebatDischarTotal" : {"value" :414, "length" : 4, "type" : "num", "divide" : 10}, - "eacDischarToday" : {"value" :422, "length" : 4, "type" : "num", "divide" : 10}, - "eacDischarTotal" : {"value" :430, "length" : 4, "type" : "num", "divide" : 10}, - "ACCharCurr" : {"value" :438, "length" : 2, "type" : "num", "divide" : 10}, - "ACDischarWatt" : {"value" :442, "length" : 4, "type" : "num", "divide" : 10}, - "ACDischarVA" : {"value" :450, "length" : 4, "type" : "num", "divide" : 10}, - "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} + "loadpercent" : {"value" :266, "length" : 2, "type" : "num", "divide" : 10}, + "buck1_ntc" : {"value" :286, "length" : 2, "type" : "num", "divide" : 10}, + "buck2_ntc" : {"value" :290, "length" : 2, "type" : "num", "divide" : 10}, + "OP_Curr" : {"value" :294, "length" : 2, "type" : "num", "divide" : 10}, + "Inv_Curr" : {"value" :298, "length" : 2, "type" : "num", "divide" : 10}, + "AC_InWatt" : {"value" :302, "length" : 4, "type" : "num", "divide" : 10}, + "AC_InVA" : {"value" :310, "length" : 4, "type" : "num", "divide" : 10}, + "faultBit" : {"value" :318, "length" : 2, "type" : "num", "divide" : 1}, + "warningBit" : {"value" :322, "length" : 2, "type" : "num", "divide" : 1}, + "faultValue" : {"value" :326, "length" : 2, "type" : "num", "divide" : 1}, + "warningValue" : {"value" :330, "length" : 2, "type" : "num", "divide" : 1}, + "constantPowerOK" : {"value" :346, "length" : 2, "type" : "num", "divide" : 1}, + "epvtoday" : {"value" :358, "length" : 4, "type" : "num", "divide" : 10}, + "pvenergytoday" : {"value" :358, "length" : 4, "type" : "num", "divide" : 10}, + "epvtotal" : {"value" :366, "length" : 4, "type" : "num", "divide" : 10}, + "eacCharToday" : {"value" :390, "length" : 4, "type" : "num", "divide" : 10}, + "eacCharTotal" : {"value" :398, "length" : 4, "type" : "num", "divide" : 10}, + "ebatDischarToday" : {"value" :406, "length" : 4, "type" : "num", "divide" : 10}, + "ebatDischarTotal" : {"value" :414, "length" : 4, "type" : "num", "divide" : 10}, + "eacDischarToday" : {"value" :422, "length" : 4, "type" : "num", "divide" : 10}, + "eacDischarTotal" : {"value" :430, "length" : 4, "type" : "num", "divide" : 10}, + "ACCharCurr" : {"value" :438, "length" : 2, "type" : "num", "divide" : 10}, + "ACDischarWatt" : {"value" :442, "length" : 4, "type" : "num", "divide" : 10}, + "ACDischarVA" : {"value" :450, "length" : 4, "type" : "num", "divide" : 10}, + "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}, + "eopDischrToday" : {"value" :506, "length" : 4, "type" : "num", "divide" : 10}, + "eopDischrTotal" : {"value" :514, "length" : 4, "type" : "num", "divide" : 10} }} self.recorddict9 = {"T06NNNNXTL3": { @@ -2324,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))) + 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 )