Skip to content

Commit

Permalink
2.2.1
Browse files Browse the repository at this point in the history
  • Loading branch information
britkat1980 committed Jun 29, 2023
1 parent 3784220 commit 828d9ee
Show file tree
Hide file tree
Showing 14 changed files with 115 additions and 43 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions GivTCP/GivLUT.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions GivTCP/mqtt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 34 additions & 26 deletions GivTCP/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ########
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
14 changes: 12 additions & 2 deletions GivTCP/write.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion buildx.bat
Original file line number Diff line number Diff line change
@@ -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 .
8 changes: 7 additions & 1 deletion config.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions givenergy_modbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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]},
Expand Down
2 changes: 1 addition & 1 deletion givenergy_modbus/modbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions givenergy_modbus/model/register_getter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime
from typing import Any

import zoneinfo
from pydantic.utils import GetterDict


Expand Down Expand Up @@ -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'):
Expand Down
2 changes: 1 addition & 1 deletion givenergy_modbus/pdu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
5 changes: 4 additions & 1 deletion startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:

Expand Down
12 changes: 12 additions & 0 deletions translations/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >-
Expand All @@ -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: >-
Expand All @@ -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: >-
Expand Down

0 comments on commit 828d9ee

Please sign in to comment.