diff --git a/CHANGELOG.md b/CHANGELOG.md index f2ede6b1..15898625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2.2.1] - 2023-06-29 +### Fixed +- Invertor Mode calculation +- Fixed Inverer Time entity +### Added +- Now compatable with the new GE AIO device (no battery data yet) +- New battery power mode switch (replicates the GE Portal "Eco" switch) + + ## [2.2.0] - 2023-06-28 ### Fixed - Type error in MQTT publishing handled gracefully diff --git a/Dockerfile b/Dockerfile index 56c4b5dd..e66d1827 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,11 +32,20 @@ COPY redis.conf redis.conf ENV NUMINVERTORS=1 ENV INVERTOR_IP_1="" ENV NUMBATTERIES_1=1 +ENV INVERTOR_AIO_1=False +ENV INVERTOR_IP_2="" +ENV NUMBATTERIES_2=1 +ENV INVERTOR_AIO_2=False +ENV INVERTOR_IP_3="" +ENV NUMBATTERIES_3=1 +ENV INVERTOR_AIO_3=False ENV MQTT_OUTPUT=True ENV MQTT_ADDRESS="" ENV MQTT_USERNAME="" ENV MQTT_PASSWORD="" ENV MQTT_TOPIC="" +ENV MQTT_TOPIC_2="" +ENV MQTT_TOPIC_3="" ENV MQTT_PORT=1883 ENV LOG_LEVEL="Info" ENV PRINT_RAW=True diff --git a/GivTCP/GivLUT.py b/GivTCP/GivLUT.py index 6bd68dba..ea1afc4c 100644 --- a/GivTCP/GivLUT.py +++ b/GivTCP/GivLUT.py @@ -3,10 +3,13 @@ def getData(fullrefresh): from givenergy_modbus.client import GivEnergyClient from settings import GiV_Settings from givenergy_modbus.model.plant import Plant - client= GivEnergyClient(host=GiV_Settings.invertorIP) - plant=Plant(number_batteries=int(GiV_Settings.numBatteries)) - client.refresh_plant(plant,full_refresh=fullrefresh) + if GiV_Settings.isAIO: + numbat=0 + else: + numbat=GiV_Settings.numBatteries + plant=Plant(number_batteries=numbat) + client.refresh_plant(plant,GiV_Settings.isAIO,full_refresh=fullrefresh) return (plant) class GivQueue: @@ -40,7 +43,7 @@ class GivLUT: import logging, os, zoneinfo from settings import GiV_Settings from logging.handlers import TimedRotatingFileHandler - logging.basicConfig(format='%(asctime)s - %(module)s - [%(levelname)s] - %(message)s') + logging.basicConfig(format='%(asctime)s - %(module)s_inv'+ str(GiV_Settings.givtcp_instance)+' - [%(levelname)-8s] - %(message)s') formatter = logging.Formatter( '%(asctime)s - %(module)s - [%(levelname)s] - %(message)s') fh = TimedRotatingFileHandler(GiV_Settings.Debug_File_Location, when='D', interval=1, backupCount=7) @@ -228,6 +231,7 @@ class GivLUT: "Charge_Time_Remaining":GEType("sensor","","",0,1000,True,False,False), "Charge_Completion_Time":GEType("sensor","timestamp","","","",False,False,False), "Discharge_Completion_Time":GEType("sensor","timestamp","","","",False,False,False), + "Battery_Power_Mode":GEType("switch","","setBatteryPowerMode","","",False,False,False), } time_slots=[ "00:00:00","00:01:00","00:02:00","00:03:00","00:04:00","00:05:00","00:06:00","00:07:00","00:08:00","00:09:00","00:10:00","00:11:00","00:12:00","00:13:00","00:14:00","00:15:00","00:16:00","00:17:00","00:18:00","00:19:00","00:20:00","00:21:00","00:22:00","00:23:00","00:24:00","00:25:00","00:26:00","00:27:00","00:28:00","00:29:00","00:30:00","00:31:00","00:32:00","00:33:00","00:34:00","00:35:00","00:36:00","00:37:00","00:38:00","00:39:00","00:40:00","00:41:00","00:42:00","00:43:00","00:44:00","00:45:00","00:46:00","00:47:00","00:48:00","00:49:00","00:50:00","00:51:00","00:52:00","00:53:00","00:54:00","00:55:00","00:56:00","00:57:00","00:58:00","00:59:00", diff --git a/GivTCP/mqtt_client.py b/GivTCP/mqtt_client.py index b0b1012e..d719afd8 100644 --- a/GivTCP/mqtt_client.py +++ b/GivTCP/mqtt_client.py @@ -75,6 +75,9 @@ def on_message(client, userdata, message): elif command=="enableDishargeSchedule": writecommand['state']=str(message.payload.decode("utf-8")) wr.enableDischargeSchedule(writecommand) + elif command=="setBatteryPowerMode": + writecommand['state']=str(message.payload.decode("utf-8")) + wr.setBatteryPowerMode(writecommand) elif command=="enableDischarge": writecommand['state']=str(message.payload.decode("utf-8")) wr.enableDischarge(writecommand) diff --git a/GivTCP/read.py b/GivTCP/read.py index 955c6d2d..c2a06d9a 100644 --- a/GivTCP/read.py +++ b/GivTCP/read.py @@ -60,14 +60,16 @@ def getData(fullrefresh): # Read from Inverter put in cache plant=GivQueue.q.enqueue(inverterData,fullrefresh,retry=Retry(max=GiV_Settings.queue_retries, interval=2)) while plant.result is None and plant.exc_info is None: time.sleep(0.5) -# plant=inverterData(True) - # Check the ojects are not empty... if "ERROR" in plant.result: raise Exception ("Garbage or failed inverter Response: "+ str(plant.result)) - GEInv=plant.result[0] GEBat=plant.result[1] +# plant=inverterData(True) +# GEInv=plant[0] +# GEBat=plant[1] + + multi_output['Last_Updated_Time'] = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat() multi_output['status'] = "online" multi_output['Time_Since_Last_Update'] = 0 @@ -245,28 +247,29 @@ def getData(fullrefresh): # Read from Inverter put in cache ######## Battery Stats only if there are batteries... ######## logger.debug("Getting SOC") - if int(GiV_Settings.numBatteries) > 0: # only do this if there are batteries - if GEInv.battery_percent != 0: - power_output['SOC'] = GEInv.battery_percent - elif GEInv.battery_percent == 0 and 'multi_output_old' in locals(): - power_output['SOC'] = multi_output_old['Power']['Power']['SOC'] - logger.error("\"Battery SOC\" reported as: "+str(GEInv.battery_percent)+"% so using previous value") - elif GEInv.battery_percent == 0 and not 'multi_output_old' in locals(): - power_output['SOC'] = 1 - logger.error("\"Battery SOC\" reported as: "+str(GEInv.battery_percent)+"% and no previou value so setting to 1%") - power_output['SOC_kWh'] = (int(power_output['SOC'])*((GEInv.battery_nominal_capacity*51.2)/1000))/100 - - # Energy Stats - energy_today_output['Battery_Charge_Energy_Today_kWh'] = GEInv.e_battery_charge_day - energy_today_output['Battery_Discharge_Energy_Today_kWh'] = GEInv.e_battery_discharge_day - energy_today_output['Battery_Throughput_Today_kWh'] = GEInv.e_battery_charge_day+GEInv.e_battery_discharge_day - energy_total_output['Battery_Throughput_Total_kWh'] = GEInv.e_battery_throughput_total - if GEInv.e_battery_charge_total == 0 and GEInv.e_battery_discharge_total == 0: # If no values in "nomal" registers then grab from back up registers - for some f/w versions - energy_total_output['Battery_Charge_Energy_Total_kWh'] = GEBat[0].e_battery_charge_total_2 - energy_total_output['Battery_Discharge_Energy_Total_kWh'] = GEBat[0].e_battery_discharge_total_2 - else: - energy_total_output['Battery_Charge_Energy_Total_kWh'] = GEInv.e_battery_charge_total - energy_total_output['Battery_Discharge_Energy_Total_kWh'] = GEInv.e_battery_discharge_total +# if int(GiV_Settings.numBatteries) > 0: # only do this if there are batteries + if GEInv.battery_percent != 0: + power_output['SOC'] = GEInv.battery_percent + elif GEInv.battery_percent == 0 and 'multi_output_old' in locals(): + power_output['SOC'] = multi_output_old['Power']['Power']['SOC'] + logger.error("\"Battery SOC\" reported as: "+str(GEInv.battery_percent)+"% so using previous value") + elif GEInv.battery_percent == 0 and not 'multi_output_old' in locals(): + power_output['SOC'] = 1 + logger.error("\"Battery SOC\" reported as: "+str(GEInv.battery_percent)+"% and no previous value so setting to 1%") + power_output['SOC_kWh'] = (int(power_output['SOC'])*((GEInv.battery_nominal_capacity*51.2)/1000))/100 + + # Energy Stats + logger.debug("Getting Battery Energy Data") + energy_today_output['Battery_Charge_Energy_Today_kWh'] = GEInv.e_battery_charge_day + energy_today_output['Battery_Discharge_Energy_Today_kWh'] = GEInv.e_battery_discharge_day + energy_today_output['Battery_Throughput_Today_kWh'] = GEInv.e_battery_charge_day+GEInv.e_battery_discharge_day + energy_total_output['Battery_Throughput_Total_kWh'] = GEInv.e_battery_throughput_total + if GEInv.e_battery_charge_total == 0 and GEInv.e_battery_discharge_total == 0 and not GiV_Settings.numBatteries==0: # If no values in "nomal" registers then grab from back up registers - for some f/w versions + energy_total_output['Battery_Charge_Energy_Total_kWh'] = GEBat[0].e_battery_charge_total_2 + energy_total_output['Battery_Discharge_Energy_Total_kWh'] = GEBat[0].e_battery_discharge_total_2 + else: + energy_total_output['Battery_Charge_Energy_Total_kWh'] = GEInv.e_battery_charge_total + energy_total_output['Battery_Discharge_Energy_Total_kWh'] = GEInv.e_battery_discharge_total ######## Get Control Data ######## @@ -281,6 +284,10 @@ def getData(fullrefresh): # Read from Inverter put in cache discharge_schedule = "enable" else: discharge_schedule = "disable" + if GEInv.battery_power_mode == 1: + batPowerMode="enable" + else: + batPowerMode="disable" #Get Battery Stat registers #battery_reserve = GEInv.battery_discharge_min_power_reserve @@ -344,6 +351,7 @@ def getData(fullrefresh): # Read from Inverter put in cache controlmode['Mode'] = mode controlmode['Battery_Power_Reserve'] = battery_reserve controlmode['Battery_Power_Cutoff'] = battery_cutoff + controlmode['Battery_Power_Mode'] = batPowerMode controlmode['Target_SOC'] = target_soc controlmode['Enable_Charge_Schedule'] = charge_schedule controlmode['Enable_Discharge_Schedule'] = discharge_schedule @@ -487,7 +495,7 @@ def getData(fullrefresh): # Read from Inverter put in cache inverter['Invertor_Serial_Number'] = GEInv.inverter_serial_number inverter['Modbus_Version'] = GEInv.modbus_version inverter['Invertor_Firmware'] = GEInv.arm_firmware_version - inverter['Invertor_Time'] = GEInv.system_time + inverter['Invertor_Time'] = GEInv.system_time.replace(tzinfo=GivLUT.timezone).isoformat() if GEInv.meter_type == 1: metertype = "EM115" if GEInv.meter_type == 0: diff --git a/GivTCP/write.py b/GivTCP/write.py index 9eb84ac4..8a2f6fbf 100644 --- a/GivTCP/write.py +++ b/GivTCP/write.py @@ -163,10 +163,10 @@ def sbdl(target): temp['result']="Setting battery charge limit failed: " + str(e) logger.error (temp['result']) return json.dumps(temp) -def smd(target): +def smd(): temp={} try: - client.set_mode_dynamic(target) + client.set_mode_dynamic() temp['result']="Setting dynamic mode was a success" logger.info(temp['result']) except: @@ -1107,6 +1107,16 @@ def tempPauseCharge(pauseTime): logger.error(temp['result']) return json.dumps(temp) +def setBatteryPowerMode(payload): + temp={} + if type(payload) is not dict: payload=json.loads(payload) + if payload['state']=="enable": + from write import sbdmd + result=GivQueue.q.enqueue(sbdmd,retry=Retry(max=GiV_Settings.queue_retries, interval=2)) + else: + from write import sbdmmp + result=GivQueue.q.enqueue(sbdmmp,retry=Retry(max=GiV_Settings.queue_retries, interval=2)) + def setBatteryMode(payload): temp={} if type(payload) is not dict: payload=json.loads(payload) diff --git a/buildx.bat b/buildx.bat index 2ebb75f7..b0484726 100644 --- a/buildx.bat +++ b/buildx.bat @@ -1,2 +1,2 @@ -docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -t britkat/giv_tcp-dev:2.2.4 --push . +docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -t britkat/giv_tcp-dev:2.2.25 --push . ::docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -t britkat/giv_tcp-ma:latest -t britkat/giv_tcp-ma:2.2.0 --push . diff --git a/config.yaml b/config.yaml index fcc7d79e..1dbc64eb 100644 --- a/config.yaml +++ b/config.yaml @@ -1,6 +1,6 @@ name: "GivTCP" description: "TCP Modbus connection to MQTT/JSON for Givenergy Battery/PV Invertors" -version: "2.2" +version: "2.2.1" image: britkat/giv_tcp-ma slug: "givtcp" homeassistant_api: true @@ -23,13 +23,16 @@ host_network: true options: NUMINVERTORS: 1 INVERTOR_IP_1: "" + INVERTOR_AIO_1: False NUMBATTERIES_1: 1 HADEVICEPREFIX: "GivTCP" INVERTOR_IP_2: "" NUMBATTERIES_2: 1 + INVERTOR_AIO_2: False HADEVICEPREFIX_2: "GivTCP2" INVERTOR_IP_3: "" NUMBATTERIES_3: 1 + INVERTOR_AIO_3: False HADEVICEPREFIX_3: "GivTCP3" MQTT_OUTPUT: True MQTT_ADDRESS: "core-mosquitto" @@ -79,12 +82,15 @@ schema: NUMINVERTORS: int INVERTOR_IP_1: str NUMBATTERIES_1: int + INVERTOR_AIO_1: bool HADEVICEPREFIX: "str?" INVERTOR_IP_2: "str?" NUMBATTERIES_2: "int?" + INVERTOR_AIO_2: bool HADEVICEPREFIX_2: "str?" INVERTOR_IP_3: "str?" NUMBATTERIES_3: "int?" + INVERTOR_AIO_3: bool HADEVICEPREFIX_3: "str?" MQTT_OUTPUT: bool MQTT_ADDRESS: str diff --git a/givenergy_modbus/client.py b/givenergy_modbus/client.py index 3c2ab67c..cc7c4664 100644 --- a/givenergy_modbus/client.py +++ b/givenergy_modbus/client.py @@ -44,7 +44,7 @@ def fetch_register_pages( register_cache.set_registers(register, data) t.sleep(sleep_between_queries) - def refresh_plant(self, plant: Plant, full_refresh: bool, sleep_between_queries=DEFAULT_SLEEP): + def refresh_plant(self, plant: Plant, isAIO: bool, full_refresh: bool, sleep_between_queries=DEFAULT_SLEEP): """Refresh the internal caches for a plant. Optionally refresh only data that changes frequently.""" inverter_registers = { InputRegister: [0, 180], @@ -53,9 +53,17 @@ def refresh_plant(self, plant: Plant, full_refresh: bool, sleep_between_queries= if full_refresh: inverter_registers[HoldingRegister] = [0, 60, 120] - self.fetch_register_pages( - inverter_registers, plant.inverter_rc, slave_address=0x31, sleep_between_queries=sleep_between_queries - ) + #How do I know which inverter I'm connecting to from inside the library... + if isAIO: + self.fetch_register_pages( + inverter_registers, plant.inverter_rc, slave_address=0x11, sleep_between_queries=sleep_between_queries + ) + _logger.debug("Inverter is AIO so using the 0x11 slave_address") + else: + self.fetch_register_pages( + inverter_registers, plant.inverter_rc, slave_address=0x31, sleep_between_queries=sleep_between_queries + ) + _logger.debug("Inverter is normal so using the 0x31 slave_address") for i, battery_rc in enumerate(plant.batteries_rcs): self.fetch_register_pages( {InputRegister: [60]}, diff --git a/givenergy_modbus/modbus.py b/givenergy_modbus/modbus.py index ec83ab63..c22aced1 100644 --- a/givenergy_modbus/modbus.py +++ b/givenergy_modbus/modbus.py @@ -110,7 +110,7 @@ def write_holding_register(self, register: HoldingRegister, value: int) -> None: if value != value & 0xFFFF: raise ValueError(f'Value {value} must fit in 2 bytes') _logger.info(f'Attempting to write {value}/{hex(value)} to Holding Register {register.value}/{register.name}') - request = WriteHoldingRegisterRequest(register=register.value, value=value) + request = WriteHoldingRegisterRequest(register=register.value, value=value, slave_address=0x11) result = self.execute(request) if isinstance(result, WriteHoldingRegisterResponse): if result.value != value: diff --git a/givenergy_modbus/model/register_getter.py b/givenergy_modbus/model/register_getter.py index a36b57fc..8bfbe4b7 100644 --- a/givenergy_modbus/model/register_getter.py +++ b/givenergy_modbus/model/register_getter.py @@ -1,6 +1,6 @@ from datetime import datetime from typing import Any - +import zoneinfo from pydantic.utils import GetterDict @@ -54,7 +54,7 @@ def get(self, key: Any, default: Any = None) -> Any: self.get('system_time_day'), self.get('system_time_hour'), self.get('system_time_minute'), - self.get('system_time_second'), + self.get('system_time_second') ) if key in ('charge_slot_1', 'charge_slot_2', 'discharge_slot_1', 'discharge_slot_2'): diff --git a/givenergy_modbus/pdu.py b/givenergy_modbus/pdu.py index 77e46b62..04cfc70a 100644 --- a/givenergy_modbus/pdu.py +++ b/givenergy_modbus/pdu.py @@ -386,11 +386,11 @@ def _update_check_code(self): crc_builder = BinaryPayloadBuilder(byteorder=Endian.Big) crc_builder.add_8bit_uint(self.slave_address) crc_builder.add_8bit_uint(self.function_code) + crc_builder.add_16bit_uint(self.register) crc_builder.add_16bit_uint(self.value) self.check = CrcModbus().process(crc_builder.to_string()).final() self.check=int.from_bytes(self.check.to_bytes(2,'little'),'big') self.builder.add_16bit_uint(self.check) - def _calculate_function_data_size(self): size = 16 _logger.debug(f"Calculated {size} bytes partial response size for {self}") diff --git a/startup.py b/startup.py index 623d675c..d2a27565 100644 --- a/startup.py +++ b/startup.py @@ -109,7 +109,8 @@ def palm_job(): with open(PATH+"/settings.py", 'w') as outp: outp.write("class GiV_Settings:\n") outp.write(" invertorIP=\""+str(os.getenv("INVERTOR_IP_"+str(inv),""))+"\"\n") - outp.write(" numBatteries=\""+str(os.getenv("NUMBATTERIES_"+str(inv),"")+"\"\n")) + outp.write(" numBatteries="+str(os.getenv("NUMBATTERIES_"+str(inv),"")+"\n")) + outp.write(" isAIO="+str(os.getenv("INVERTOR_AIO_"+str(inv),"")+"\n")) outp.write(" Print_Raw_Registers="+str(os.getenv("PRINT_RAW",""))+"\n") outp.write(" MQTT_Output="+str(os.getenv("MQTT_OUTPUT","")+"\n")) if hasMQTT: @@ -160,6 +161,8 @@ def palm_job(): else: outp.write(" cache_location=\""+str(os.getenv("CACHELOCATION")+"\"\n")) outp.write(" Debug_File_Location=\""+os.getenv("CACHELOCATION")+"/log_inv_"+str(inv)+".log\"\n") + outp.write(" inverter_num=\""+str(inv)+"\"\n") + # replicate the startup script here: diff --git a/translations/en.yaml b/translations/en.yaml index d400cfe1..c66c7eac 100644 --- a/translations/en.yaml +++ b/translations/en.yaml @@ -12,6 +12,10 @@ configuration: name: Inverter 1 - How many connected batteries? description: >- How many batteries are directly connected to inverter 1? + INVERTOR_AIO_1: + name: Inverter 1 - Is it an AIO? + description: >- + Does this firmware need the new modubs connection to work, only applies to AIO and future firmware releases. If unsure, leave it as False/off HADEVICEPREFIX: name: Inverter 1 - Home Assistant Entity Prefix description: >- @@ -24,6 +28,10 @@ configuration: name: Inverter 2 - How many connected batteries? description: >- How many batteries are directly connected to inverter 2? + INVERTOR_AIO_2: + name: Inverter 2 - Is it an AIO? + description: >- + Does this firmware need the new modubs connection to work, only applies to AIO and future firmware releases. If unsure, leave it as False/off HADEVICEPREFIX_2: name: Inverter 2 - Home Assistant Entity Prefix description: >- @@ -36,6 +44,10 @@ configuration: name: Inverter 3 - How many connected batteries? description: >- How many batteries are directly connected to inverter 3? + INVERTOR_AIO_3: + name: Inverter 3 - Is it an AIO? + description: >- + Does this firmware need the new modubs connection to work, only applies to AIO and future firmware releases. If unsure, leave it as False/off HADEVICEPREFIX_3: name: Inverter 3 - Home Assistant Entity Prefix description: >-