diff --git a/code/Python/classic_mqtt.py b/code/Python/classic_mqtt.py old mode 100644 new mode 100755 index a18cab1..de93528 --- a/code/Python/classic_mqtt.py +++ b/code/Python/classic_mqtt.py @@ -39,6 +39,8 @@ MQTT_MAX_ERROR_COUNT = 300 #Number of errors on the MQTT before the tool exits MAIN_LOOP_SLEEP_SECS = 5 #Seconds to sleep in the main loop +HA_ENABLED = False #Home-Assistant Auto Discovery + # --------------------------------------------------------------------------- # # Default startup values. Can be over-ridden by command line options. # --------------------------------------------------------------------------- # @@ -53,7 +55,9 @@ 'mqttPassword':os.getenv('MQTT_PASS', "ClassicPub123"), \ 'awakePublishRate':int(os.getenv('AWAKE_PUBLISH_RATE', str(DEFAULT_WAKE_RATE))), \ 'snoozePublishRate':int(os.getenv('SNOOZE_PUBLISH_RATE', str(DEFAULT_SNOOZE_RATE))), \ - 'awakePublishLimit':int(os.getenv('AWAKE_PUBLISH_LIMIT', str(DEFAULT_WAKE_PUBLISHES)))} + 'awakePublishLimit':int(os.getenv('AWAKE_PUBLISH_LIMIT', str(DEFAULT_WAKE_PUBLISHES))), \ + 'homeassistant':os.getenv('HA_ENABLED', str(HA_ENABLED)) \ + } # --------------------------------------------------------------------------- # # Counters and status variables @@ -72,6 +76,12 @@ snoozeCycleLimit = 0 #How many cycles before I publish in snooze mode (changes with wake rate) currentPollRate = DEFAULT_WAKE_RATE mqttClient = None +homeassistantEnabled = False + +mqttDeviceModel = 'Classic' +mqttDeviceFirmware = '' +mqttLastSOCicon = '' +mqttLastCSicon = '' # --------------------------------------------------------------------------- # # configure the logging @@ -88,9 +98,11 @@ # MQTT On Connect function # --------------------------------------------------------------------------- # def on_connect(client, userdata, flags, rc): - global mqttConnected, mqttErrorCount, mqttClient + global mqttConnected, mqttErrorCount, mqttClient, mqttDeviceModel if rc==0: log.debug("MQTT connected OK Returned code={}".format(rc)) + # re-initiate HA-autodiscovery + infoPublished = False #subscribe to the commands try: topic = "{}{}/cmnd/#".format(argumentValues['mqttRoot'], argumentValues['classicName']) @@ -100,6 +112,7 @@ def on_connect(client, userdata, flags, rc): #publish that we are Online will_topic = "{}{}/tele/LWT".format(argumentValues['mqttRoot'], argumentValues['classicName']) mqttClient.publish(will_topic, "Online", qos=0, retain=False) + except Exception as e: log.error("MQTT Subscribe failed") log.exception(e, exc_info=True) @@ -190,6 +203,142 @@ def mqttPublish(client, data, subtopic): mqttConnected = False return False +def mqttHApublish( sensor, name, units, icon, inforead, vtemplate, data ): + #publisch HA autodiscovery for 1 sensor/diagnostic + global mqttClient, argumentValues, mqttDeviceModel, mqttDeviceFirmware + # + HA_root = argumentValues['mqttRoot'] + HA_name = argumentValues['classicName'] + HA_device = '"force_update": "true", "device": {{ "identifiers": ["{}"],"name": "{}","manufacturer": "MidNite-Solar","model": "{}", "sw_version": "{}"}}'.format( HA_name, HA_name, mqttDeviceModel, mqttDeviceFirmware ) + # Vtemplate + HA_vtemplate = '{{{{value_json.{0}}}}}'.format(sensor) + if vtemplate != '': + HA_vtemplate = vtemplate + # Units + HA_units = units + if units == 'C': + HA_icon = '"icon": "mdi:thermometer", ' + if icon != '': + HA_icon = icon + icon = '' + HA_units = '"unit_of_meas": "Ā°C", '+HA_icon+'"device_class": "temperature", "state_class": "measurement", ' + if units == 'A': + HA_units = '"unit_of_meas": "A", "device_class": "power", "state_class": "measurement", ' + if units == 'V': + HA_units = '"unit_of_meas": "V", "device_class": "power", "state_class": "measurement", ' + if units == 'W': + HA_units = '"unit_of_meas": "W", "device_class": "power", "state_class": "measurement", ' + if units == 'kWh': + HA_units = '"unit_of_meas": "kWh", "device_class": "power", "state_class": "measurement", ' + if units == '%': + HA_icon = '' # '"icon": "mdi:battery", ' + if icon != '': + HA_icon = icon + icon = '' + HA_units = '"unit_of_meas": "%", '+HA_icon+'"state_class": "measurement", ' + if units == 's': + HA_icon = '"icon": "mdi:clock", ' + if icon != '': + HA_icon = icon + icon = '' + HA_units = '"unit_of_meas": "s", '+HA_icon+'"state_class": "measurement", ' + if units == 'Ah': + HA_units = '"unit_of_meas": "Ah", "device_class": "power", "state_class": "measurement", ' + # + HA_topic = "homeassistant/sensor/{}/{}/config".format(HA_name, sensor) + HA_msg = '{{"~": "{0}", "unique_id": "{0}-{1}", "object_id": "{0}-{1}", "name": "{2}", {3}{4}"state_topic": "{5}{0}/stat/{6}", "value_template": "{8}", {7}}}'.format( + HA_name, sensor, name, icon, HA_units, HA_root, inforead, HA_device, HA_vtemplate ) + # 0 1 2 3 4 5 6 7 8 + #log.debug( "publish: {}".format(HA_msg) ) + mqttClient.publish(HA_topic, HA_msg, qos=0, retain=False) + # + +def mqttHAautodiscovery( data ): + # publisch HA autodiscovery + global mqttClient, argumentValues, mqttDeviceModel, mqttDeviceFirmware + # + mqttDeviceModel = "Classic {}V (rev {})".format(data["Type"],data["PCB"]) + mqttDeviceFirmware = "{:04n}{:02n}{:02n}.app.{}.net.{}".format(data["Year"],data["Month"],data["Day"],data['app_rev'],data['net_rev']) + # + # Device info + mqttHApublish( 'model', 'device Model', '"entity_category": "diagnostic", ', '"icon": "mdi:teddy-bear", ', 'info', '', data ) + mqttHApublish( 'deviceName', 'device Name', '"entity_category": "diagnostic", ', '"icon": "mdi:home-analytics", ', 'info', '', data ) + mqttHApublish( 'deviceType', 'device Type', '"entity_category": "diagnostic", ', '"icon": "mdi:format-list-bulleted-type", ', 'info', '', data ) + mqttHApublish( 'macAddress', 'MAC Address', '"entity_category": "diagnostic", ', '"icon": "mdi:console-network", ', 'info', '', data ) + mqttHApublish( 'IP', 'IP Address', '"entity_category": "diagnostic", ', '"icon": "mdi:ip-network", ', 'info', '', data ) + mqttHApublish( 'nominalBatteryVoltage', 'nominal Battery Voltage', '"entity_category": "diagnostic", "unit_of_meas": "V", ', '"icon": "mdi:battery-charging", ', 'info', '', data ) + # Measurements + mqttHApublish( 'BatTemperature', 'Temperature Battery', 'C', '', 'readings', '', data ) + mqttHApublish( 'PCBTemperature', 'Temperature PCB', 'C', '', 'readings', '', data ) + mqttHApublish( 'FETTemperature', 'Temperature FET', 'C', '', 'readings', '', data ) + mqttHApublish( 'ShuntTemperature', 'Temperature Shunt', 'C', '', 'readings', '', data ) + mqttHApublish( 'PVCurrent', 'PV Current', 'A', '"icon": "mdi:solar-panel", ', 'readings', '', data ) + mqttHApublish( 'Power', 'PV Power', 'W', '"icon": "mdi:solar-panel", ', 'readings', '', data ) + mqttHApublish( 'PVVoltage', 'PV Voltage', 'V', '"icon": "mdi:solar-panel", ', 'readings', '', data ) + mqttHApublish( 'BatVoltage', 'Battery Voltage', 'V', '', 'readings', '', data ) + mqttHApublish( 'BatCurrent', 'Battery Current', 'A', '', 'readings', '', data ) + mqttHApublish( 'WhizbangBatCurrent', 'Battery Current Whizbang', 'A', '', 'readings', '', data ) + mqttHApublish( 'SOC', 'Charge SOC', '"unit_of_meas": "%", "state_class": "measurement", ', '"icon": "'+data['SOCicon']+'", ', 'readings', '', data ) + mqttHApublish( 'RemainingAmpHours', 'Amp Hours Remaining', 'Ah', '', 'readings', '', data ) + mqttHApublish( 'TotalAmpHours', 'Amp Hours Total', 'Ah', '', 'readings', '', data ) + mqttHApublish( 'NetAmpHours', 'Amp Hours Netto', 'Ah', '', 'readings', '', data ) + mqttHApublish( 'EnergyToday', 'Energy Today', 'kWh', '"icon": "mdi:calendar-today", ', 'readings', '', data ) + mqttHApublish( 'TotalEnergy', 'Energy Total', 'kWh', '"icon": "mdi:home-lightning-bolt-outline", ', 'readings', '', data ) + mqttHApublish( 'currentTime', 'Current Time', '"state_class": "measurement", ', '"icon": "mdi:calendar-clock", ', 'readings', '', data ) + mqttHApublish( 'ChargeState', 'Charge State', '', '"icon": "'+data['ChargeStateIcon']+'", ', 'readings', '', data ) + mqttHApublish( 'ChargeStateText', 'Charge State Text', '', '"icon": "'+data['ChargeStateIcon']+'", ', 'readings', '', data ) + #mqttHApublish( 'ChargeStateText', 'Charge State Text', '', '"icon": "'+data['ChargeStateIcon']+'", ', 'readings', '{{ {0: \'Resting\',3: \'Absorb\',4: \'Bulk MPPT\',5: \'Float\',6: \'Float MPPT\',7: \'Equalize\',10: \'HyperVOC\',18: \'Equalize MPPT\'}[value_json.ChargeState]}}', data ) + mqttHApublish( 'FloatTimeTodaySeconds', 'Today Float Time', 's', '', 'readings', '', data ) + mqttHApublish( 'AbsorbTime', 'Today Absorb Time', 's', '', 'readings', '', data ) + mqttHApublish( 'EqualizeTime', 'Today Equalize Time', 's', '', 'readings', '', data ) + mqttHApublish( 'ReasonForResting', 'Reason For Resting', '"state_class": "measurement", ', '', 'readings', '', data ) + mqttHApublish( 'ReasonForRestingText', 'Reason Text', '"state_class": "measurement", ', '', 'readings', '', data ) +# { +# "appVersion": 1849, +# "deviceName": "CLASSIC\u0000", < 1 char too much / stop on 0 +# "buildDate": "Monday, April 21, 2014", +# "deviceType": "Classic", +# "endingAmps": 4, +# "hasWhizbang": true, +# "lastVOC": 39.6, +# "model": "Classic 150V (rev 4)", +# "mpptMode": 9, +# "netVersion": 1839, +# "nominalBatteryVoltage": 12, +# "unitID": -1966686451, +# "macAddress": "60:1D:0F:00:36:80" +# } +# { +# "BatTemperature": 8.1, +# "NetAmpHours": -172, +# "ChargeState": 4, +# "InfoFlagsBits": -1577046016, +# "ReasonForResting": 5, +# "NegativeAmpHours": -59292, +# "BatVoltage": 13.4, +# "PVVoltage": 32.7, +# "VbattRegSetPTmpComp": 15.1, +# "TotalAmpHours": 908, +# "WhizbangBatCurrent": 12, +# "BatCurrent": 17.5, +# "PVCurrent": 7, +# "ConnectionState": 0, +# "EnergyToday": 0.2, +# "EqualizeTime": 14400, +# "SOC": 78, +# "Aux1": false, +# "Aux2": false, +# "Power": 233, +# "FETTemperature": 40.4, +# "PositiveAmpHours": 438335, +# "TotalEnergy": 2982.7, +# "FloatTimeTodaySeconds": 0, +# "RemainingAmpHours": 714, +# "AbsorbTime": 18000, +# "ShuntTemperature": 10, +# "PCBTemperature": 30.4 +# } + # --------------------------------------------------------------------------- # # Test to see if it is time to gather data and publish. # periodic is called every second, so this method figures out if it is time to @@ -231,7 +380,7 @@ def timeToPublish(): # --------------------------------------------------------------------------- # def periodic(modbus_stop): - global mqttClient, modbusErrorCount, infoPublished, mqttErrorCount, currentPollRate + global mqttClient, modbusErrorCount, infoPublished, mqttErrorCount, currentPollRate, mqttLastSOCicon, mqttLastCSicon, homeassistantEnabled if not modbus_stop.is_set(): #Get the current time as a float of seconds. @@ -244,14 +393,37 @@ def periodic(modbus_stop): #Get the Modbus Data and store it. data = getModbusData(modeAwake, argumentValues['classicHost'], argumentValues['classicPort']) if data: # got data + # modbusErrorCount = 0 - + if (not infoPublished): #Check if the Info has been published yet + # + if ( argumentValues['homeassistant'] == True ): #Check if HA_enabled is true + log.debug("Call mqttHAautodiscovery" ) + mqttHAautodiscovery( data ) + # wait 1 second for HA to receive and create device + time.sleep(1) + log.debug("Done mqttHAautodiscovery" ) + # + if mqttPublish(mqttClient,encodeClassicData_info(data),"info"): + infoPublished = True + time.sleep(1) + else: + mqttErrorCount += 1 + # if mqttPublish(mqttClient,encodeClassicData_readings(data),"readings"): - if (not infoPublished): #Check if the Info has been published yet - if mqttPublish(mqttClient,encodeClassicData_info(data),"info"): - infoPublished = True - else: - mqttErrorCount += 1 + # + if ( argumentValues['homeassistant'] == True ): #Check if HA_enabled is true + # re-send ChargeState because of icon + if mqttLastCSicon != data["ChargeStateIcon"]: + mqttLastCSicon = data["ChargeStateIcon"] + log.debug("Call CS mqttHApublish {}".format(mqttLastCSicon) ) + mqttHApublish( 'ChargeState', 'Charge State', '', '"icon": "'+ data["ChargeStateIcon"] + '", ', 'readings', '', data ) + mqttHApublish( 'ChargeStateText', 'Charge State Text', '', '"icon": "'+data['ChargeStateIcon']+'", ', 'readings', '{{ {0: \'Resting\',3: \'Absorb\',4: \'Bulk MPPT\',5: \'Float\',6: \'Float MPPT\',7: \'Equalize\',10: \'HyperVOC\',18: \'Equalize MPPT\'}[value_json.ChargeState]}}', data ) + # re-send SOC because of icon + if mqttLastSOCicon != data["SOCicon"]: + mqttLastSOCicon = data["SOCicon"] + log.debug("Call SOC mqttHApublish {}".format(mqttLastSOCicon) ) + mqttHApublish( 'SOC', 'Charge SOC', '"unit_of_meas": "%", "state_class": "measurement", ', '"icon": "'+ data["SOCicon"] + '", ', 'readings', '', data ) else: mqttErrorCount += 1 @@ -280,7 +452,7 @@ def periodic(modbus_stop): # --------------------------------------------------------------------------- # def run(argv): - global doStop, mqttClient, awakePublishCycles, snoozePublishCycles, currentPollRate, snoozeCycleLimit + global doStop, mqttClient, awakePublishCycles, snoozePublishCycles, currentPollRate, snoozeCycleLimit, mqttLastSOCicon, mqttLastCSicon, homeassistantEnabled log.info("classic_mqtt starting up...") @@ -295,6 +467,8 @@ def run(argv): currentPollRate = argumentValues['awakePublishRate'] + homeassistantEnabled = argumentValues['homeassistant'] + #random seed from the OS seed(int.from_bytes( os.urandom(4), byteorder="big")) diff --git a/code/Python/support/classic_jsonencoder.py b/code/Python/support/classic_jsonencoder.py old mode 100644 new mode 100755 index 814bb1f..2b33dd7 --- a/code/Python/support/classic_jsonencoder.py +++ b/code/Python/support/classic_jsonencoder.py @@ -20,17 +20,22 @@ def encodeClassicData_readings(decoded): #log.debug("Enter encodeClassicData_readings") classicData = {} - + + classicData["currentTime"] = decodeCTIME( decoded["CTIME0"],decoded["CTIME1"], decoded["CTIME2"] ) + # "BatTemperature":-1.99, classicData["BatTemperature"] = decoded["BatTemperature"] # "NetAmpHours":0, classicData["NetAmpHours"] = decoded["WbJrAmpHourNET"] # "ChargeState":0, classicData["ChargeState"] = decoded["ChargeStage"] #it is mis-labeled in the ESP32 code + classicData["ChargeStateIcon"] = decoded["ChargeStateIcon"] + classicData["ChargeStateText"] = decoded["ChargeStateText"] # "InfoFlagsBits":-1308610300, classicData["InfoFlagsBits"] = decoded["InfoFlagsBits"] # "ReasonForResting":104, classicData["ReasonForResting"] = decoded["ReasonForResting"] + classicData["ReasonForRestingText"] = decoded["ReasonForRestingText"] # "NegativeAmpHours":-9170, classicData["NegativeAmpHours"] = decoded["WbJrAmpHourNEGative"] # "BatVoltage":25.21, @@ -55,7 +60,8 @@ def encodeClassicData_readings(decoded): classicData["EqualizeTime"] = decoded["EqualizeTime"] # "SOC":99, classicData["SOC"] = decoded["SOC"] - # "Aux1":false, + classicData["SOCicon"] = decoded["SOCicon"] + # "Aux1":false, classicData["Aux1"] = ((decoded["InfoFlagsBits"] & 0x00004000) != 0) # "Aux2":false, classicData["Aux2"] = ((decoded["InfoFlagsBits"] & 0x00008000) != 0) @@ -79,7 +85,7 @@ def encodeClassicData_readings(decoded): classicData["PCBTemperature"] = decoded["PCBTemperature"] return json.dumps(classicData, sort_keys=False, separators=(',', ':')) - + # --------------------------------------------------------------------------- # # Handle creating the Json for Info # --------------------------------------------------------------------------- # @@ -92,9 +98,8 @@ def encodeClassicData_info(decoded): #Assemble the string uint_array = [decoded["Name1"],decoded["Name0"],decoded["Name3"],decoded["Name2"],decoded["Name5"],decoded["Name4"],decoded["Name7"],decoded["Name6"]] # "deviceName":"CLASSIC", - classicData["deviceName"] = "".join(chr(x) for x in uint_array) + classicData["deviceName"] = "".join(chr(x) for x in uint_array).strip(chr(0)) # "buildDate":"Tuesday, February 6, 2018", - bdate = datetime.date(decoded["Year"],decoded["Month"],decoded["Day"]) classicData["buildDate"] = bdate.strftime("%A, %B %d, %Y").replace(' 0', ' ') # get rid of the stupid leading 0 in date. # "deviceType":"Classic", classicData["deviceType"] = "Classic" @@ -119,4 +124,22 @@ def encodeClassicData_info(decoded): mac = "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}".format(decoded["mac_5"],decoded["mac_4"],decoded["mac_3"],decoded["mac_2"],decoded["mac_1"],decoded["mac_0"]) classicData["macAddress"] = mac.upper() + classicData["IP"] = decoded["IP"] + return json.dumps(classicData, sort_keys=False, separators=(',', ':')) + +def decodeCTIME( CTIME0, CTIME1, CTIME2): + + # CTIME0 - BITS 5:0 seconds 13:8 minutes 20:16 Hours 26:24 DayOfWeek + tsecs = (CTIME0 & 0x0000003F) + tmins = (CTIME0 & 0x00003F00) >> 8 + thour = (CTIME0 & 0x001F0000) >> 16 + tdyow = (CTIME0 & 0x07000000) >> 24 + # CTIME1 - BITS 4:0 day of month 11:8 month 27:16 year + tdyom = (CTIME1 & 0x0000001F) + tmnth = (CTIME1 & 0x00000F00) >> 8 + tyear = (CTIME1 & 0x0FFF0000) >> 16 + # CTIME2 - BITS 11:0 day of year + tdyoy = (CTIME2 & 0x000007FF) # 1FF + # + return "{:04n}-{:02n}-{:02n} {:02n}:{:02n}:{:02n}".format(tyear,tmnth,tdyom,thour,tmins,tsecs) diff --git a/code/Python/support/classic_modbusdecoder.py b/code/Python/support/classic_modbusdecoder.py old mode 100644 new mode 100755 index 948e129..9bdeed1 --- a/code/Python/support/classic_modbusdecoder.py +++ b/code/Python/support/classic_modbusdecoder.py @@ -10,10 +10,10 @@ from pymodbus.constants import Endian from pymodbus.payload import BinaryPayloadDecoder try: - from pymodbus.client import ModbusTcpClient as ModbusClient + from pymodbus.client import ModbusTcpClient as ModbusClient # pymodbus 3 MODBUS_VERSION = 3 except ImportError: - from pymodbus.client.sync import ModbusTcpClient as ModbusClient + from pymodbus.client.sync import ModbusTcpClient as ModbusClient # pymodbus 2 MODBUS_VERSION = 2 from collections import OrderedDict import logging @@ -44,10 +44,16 @@ def getRegisters(theClient, addr, count): # Return a decoder for the passed in registers # --------------------------------------------------------------------------- # def getDataDecoder(registers): - return BinaryPayloadDecoder.fromRegisters( - registers, - byteorder=Endian.Big, - wordorder=Endian.Little) + if MODBUS_VERSION == 2: + return BinaryPayloadDecoder.fromRegisters( + registers, + byteorder=Endian.Big, + wordorder=Endian.Little) + else: + return BinaryPayloadDecoder.fromRegisters( + registers, + byteorder=Endian.BIG, + wordorder=Endian.LITTLE) # --------------------------------------------------------------------------- # # Based on the address, return the decoded OrderedDict @@ -69,7 +75,7 @@ def doDecode(addr, decoder): ('mac_5', decoder.decode_8bit_uint()), #4108 MSB ('mac_4', decoder.decode_8bit_uint()), #4108 LSB ('ignore2', decoder.skip_bytes(4)), #4109, 4110 - ('unitID', decoder.decode_32bit_int()), #4111 + ('unitID', decoder.decode_32bit_uint()), #4111 ('StatusRoll', decoder.decode_16bit_uint()), #4113 ('RsetTmms', decoder.decode_16bit_uint()), #4114 ('BatVoltage', decoder.decode_16bit_int()/10.0), #4115 @@ -86,7 +92,7 @@ def doDecode(addr, decoder): ('AmpHours', decoder.decode_16bit_uint()), #4125 ('TotalEnergy', decoder.decode_32bit_uint()/10.0), #4126, 4127 ('LifetimeAmpHours', decoder.decode_32bit_uint()), #4128, 4129 - ('InfoFlagsBits', decoder.decode_32bit_int()), #4130, 31 + ('InfoFlagsBits', decoder.decode_32bit_uint()), #4130, 31 ('BatTemperature', decoder.decode_16bit_int()/10.0), #4132 ('FETTemperature', decoder.decode_16bit_int()/10.0), #4133 ('PCBTemperature', decoder.decode_16bit_int()/10.0), #4134 @@ -124,14 +130,20 @@ def doDecode(addr, decoder): ]) elif (addr == 4209): decoded = OrderedDict([ - ('Name0', decoder.decode_8bit_uint()), #4210 - ('Name1', decoder.decode_8bit_uint()), #4211 - ('Name2', decoder.decode_8bit_uint()), #4212 - ('Name3', decoder.decode_8bit_uint()), #4213 - ('Name4', decoder.decode_8bit_uint()), #4214 - ('Name5', decoder.decode_8bit_uint()), #4215 - ('Name6', decoder.decode_8bit_uint()), #4216 - ('Name7', decoder.decode_8bit_uint()), #4217 + ('Name0', decoder.decode_8bit_uint()), #4210-MSB + ('Name1', decoder.decode_8bit_uint()), #4210-LSB + ('Name2', decoder.decode_8bit_uint()), #4211-MSB + ('Name3', decoder.decode_8bit_uint()), #4211-LSB + ('Name4', decoder.decode_8bit_uint()), #4212-MSB + ('Name5', decoder.decode_8bit_uint()), #4212-LSB + ('Name6', decoder.decode_8bit_uint()), #4213-MSB + ('Name7', decoder.decode_8bit_uint()), #4213-LSB + ]) + elif (addr == 4213): + decoded = OrderedDict([ + ('CTIME0', decoder.decode_32bit_uint()), #4214+#4215 + ('CTIME1', decoder.decode_32bit_uint()), #4216+#4217 + ('CTIME2', decoder.decode_32bit_uint()), #4218+#4219 ]) elif (addr == 4243): decoded = OrderedDict([ @@ -175,6 +187,7 @@ def getModbusData(modeAwake, classicHost, classicPort): result = modbusClient.read_holding_registers(4163, 2, unit=10) else: result = modbusClient.read_holding_registers(4163, 2, slave=10) + if result.isError(): # close the client log.error("MODBUS isError H:{} P:{}".format(classicHost, classicPort)) @@ -190,6 +203,7 @@ def getModbusData(modeAwake, classicHost, classicPort): theData[4360] = getRegisters(theClient=modbusClient,addr=4360,count=22) theData[4163] = getRegisters(theClient=modbusClient,addr=4163,count=2) theData[4209] = getRegisters(theClient=modbusClient,addr=4209,count=4) + theData[4213] = getRegisters(theClient=modbusClient,addr=4213,count=6) theData[4243] = getRegisters(theClient=modbusClient,addr=4243,count=32) theData[16386]= getRegisters(theClient=modbusClient,addr=16386,count=4) @@ -219,4 +233,80 @@ def getModbusData(modeAwake, classicHost, classicPort): for index in theData: decoded = {**dict(decoded), **dict(doDecode(index, getDataDecoder(theData[index])))} - return decoded \ No newline at end of file + # Device type 251 is different + if decoded['Type'] == 251: + decoded['Type'] = '250 KS' + + # IP number + decoded['IP'] = classicHost + + # Charge State icon + decoded['ChargeStateIcon'] = 'mdi:music-rest-whole' + if decoded['ChargeStage'] == 3 or decoded['ChargeStage'] == 4: + decoded['ChargeStateIcon'] = 'mdi:battery-charging' + elif decoded['ChargeStage'] == 5 or decoded['ChargeStage'] == 6: + decoded['ChargeStateIcon'] = 'mdi:format-float-center' + elif decoded['ChargeStage'] >= 7: + decoded['ChargeStateIcon'] = 'mdi:approximately-equal' + # + chrg_stt_txt_arr = { + 0: 'Resting', + 3: 'Absorb', + 4: 'Bulk MPPT', + 5: 'Float', + 6: 'Float MPPT', + 7: 'Equalize', + 10: 'HyperVOC', + 18: 'Equalize MPPT', + } + decoded['ChargeStateText'] = chrg_stt_txt_arr[decoded['ChargeStage']] + + # SOC icon + SOCicon = "mdi:battery-" + if decoded["ChargeStage"] == 3 or decoded["ChargeStage"] == 4: + SOCicon = SOCicon + "charging-" + SOCicon = SOCicon + str( int( int(decoded["SOC"]) / 10 ) ) + "0" + if SOCicon == "mdi:battery-100": + SOCicon = "mdi:battery" + decoded["SOCicon"] = SOCicon + # Rest reason + rest_reason_arr = { + 1: "Anti-Click. Not enough power available (Wake Up)", + 2: " Insane Ibatt Measurement (Wake Up)", + 3: " Negative Current (load on PV input ?) (Wake Up)", + 4: " PV Input Voltage lower than Battery V (Vreg state)", + 5: " Too low of power out and Vbatt below set point for > 90 seconds", + 6: " FET temperature too high (Cover is on maybe?)", + 7: " Ground Fault Detected", + 8: " Arc Fault Detected", + 9: " Too much negative current while operating (backfeed from battery out of PV input)", + 10: "Battery is less than 8.0 Volts", + 11: "PV input is available but V is rising too slowly. Low Light or bad connection(Solar mode)", + 12: "Voc has gone down from last Voc or low light. Re-check (Solar mode)", + 13: "Voc has gone up from last Voc enough to be suspicious. Re-check (Solar mode)", + 14: "PV input is available but V is rising too slowly. Low Light or bad connection(Solar mode)", + 15: "Voc has gone down from last Voc or low light. Re-check (Solar mode)", + 16: "Mppt MODE is OFF (Usually because user turned it off)", + 17: "PV input is higher than operation range (too high for 150V Classic)", + 18: "PV input is higher than operation range (too high for 200V Classic)", + 19: "PV input is higher than operation range (too high for 250V or 250KS)", + 22: "Average Battery Voltage is too high above set point", + 25: "Battery Voltage too high of Overshoot (small battery or bad cable ?)", + 26: "Mode changed while running OR Vabsorb raised more than 10.0 Volts at once OR Nominal Vbatt changed by modbus command AND MpptMode was ON when changed", + 27: "bridge center == 1023 (R132 might have been stuffed) This turns MPPT Mode to OFF", + 28: "NOT Resting but RELAY is not engaged for some reason", + 29: "ON/OFF stays off because WIND GRAPH is illegal (current step is set for > 100 amps)", + 30: "PkAmpsOverLimitā€¦ Software detected too high of PEAK output current", + 31: "AD1CH.IbattMinus > 900 Peak negative battery current > 90.0 amps (Classic 250)", + 32: "Aux 2 input commanded Classic off. for HI or LO (Aux2Function == 15 or 16)", + 33: "OCP in a mode other than Solar or PV-Uset", + 34: "AD1CH.IbattMinus > 900 Peak negative battery current > 90.0 amps (Classic 150, 200)", + 35: "Battery voltage is less than Low Battery Disconnect (LBD) Typically Vbatt is less than 8.5 volts", + 104: "104?=14?: PV input is available but V is rising too slowly. Low Light or bad connection(Solar mode)", + } + try: + decoded["ReasonForRestingText"] = rest_reason_arr[decoded["ReasonForResting"]] + except: + log.error("ReasonForRestingText Error ") + + return decoded diff --git a/code/Python/support/classic_validate.py b/code/Python/support/classic_validate.py index ddd169d..0d00dc4 100644 --- a/code/Python/support/classic_validate.py +++ b/code/Python/support/classic_validate.py @@ -70,14 +70,15 @@ def handleArgs(argv,argVals): "mqtt_pass=", "wake_publish_rate=", "snooze_publish_rate=", - "wake_publishes="]) + "wake_publishes=", + "homeassistant"]) except getopt.GetoptError: - print("Error parsing command line parameters, please use: py --classic <{}> --classic_port <{}> --classic_name <{}> --mqtt <{}> --mqtt_port <{}> --mqtt_root <{}> --mqtt_user --mqtt_pass --wake_publish_rate <{}> --snooze_publish_rate <{}> --wake_publishes <{}>".format( \ + print("Error parsing command line parameters, please use: py --classic <{}> --classic_port <{}> --classic_name <{}> --mqtt <{}> --mqtt_port <{}> --mqtt_root <{}> --mqtt_user --mqtt_pass --wake_publish_rate <{}> --snooze_publish_rate <{}> --wake_publishes <{}> --homeassistant".format( \ argVals['classicHost'], argVals['classicPort'], argVals['classicName'], argVals['mqttHost'], argVals['mqttPort'], argVals['mqttRoot'], argVals['awakePublishRate'], argVals['snoozePublishRate'], int(argVals['awakePublishLimit']*argVals['awakePublishRate']))) sys.exit(2) for opt, arg in opts: if opt == '-h': - print ("Parameter help: py --classic <{}> --classic_port <{}> --classic_name <{}> --mqtt <{}> --mqtt_port <{}> --mqtt_root <{}> --mqtt_user --mqtt_pass --wake_publish_rate <{}> --snooze_publish_rate <{}> --wake_publishes <{}>".format( \ + print ("Parameter help: py --classic <{}> --classic_port <{}> --classic_name <{}> --mqtt <{}> --mqtt_port <{}> --mqtt_root <{}> --mqtt_user --mqtt_pass --wake_publish_rate <{}> --snooze_publish_rate <{}> --wake_publishes <{}> --homeassistant".format( \ argVals['classicHost'], argVals['classicPort'], argVals['classicName'], argVals['mqttHost'], argVals['mqttPort'], argVals['mqttRoot'], argVals['awakePublishRate'], argVals['snoozePublishRate'], int(argVals['awakePublishLimit']*argVals['awakePublishRate']))) sys.exit() elif opt in ('--classic'): @@ -102,6 +103,8 @@ def handleArgs(argv,argVals): argVals['snoozePublishRate'] = int(validateIntParameter(arg,"snooze_publish_rate", argVals['snoozePublishRate'])) elif opt in ("--wake_publishes"): argVals['awakePublishLimit'] = int(validateIntParameter(arg,"wake_publishes", argVals['awakePublishLimit'])) + elif opt in ("--homeassistant"): + argVals['homeassistant'] = True #Validate the wake/snooze stuff if (argVals['snoozePublishRate'] < argVals['awakePublishRate']): @@ -128,6 +131,7 @@ def handleArgs(argv,argVals): argVals['mqttUser'] = argVals['mqttUser'].strip() + log.info("homeassistant = {}".format(argVals['homeassistant'])) log.info("classicHost = {}".format(argVals['classicHost'])) log.info("classicPort = {}".format(argVals['classicPort']))