diff --git a/sofar-hyd-ep.json b/sofar-hyd-ep.json index 1ebb136..3fcee91 100644 --- a/sofar-hyd-ep.json +++ b/sofar-hyd-ep.json @@ -7,12 +7,23 @@ "registers": 7, "ha": { "name": "Serial Number", - "icon": "mdi:numeric", + "icon": "mdi:identifier", "entity_category": "diagnostic", "value_template": "{{ value_json.serial_number }}" }, "refresh": 86400 }, + { + "name": "model", + "value": "HYD-6000-EP", + "read_type": "static", + "ha": { + "name": "Model", + "icon": "mdi:tag-text", + "value_template": "{{ value_json.model }}" + }, + "refresh": 86400 + }, { "name": "hw_version", "register": "0x044D", diff --git a/sofar-me-3000.json b/sofar-me-3000.json index b187e12..c055a4c 100644 --- a/sofar-me-3000.json +++ b/sofar-me-3000.json @@ -1,113 +1,705 @@ { - "registers": [ - { - "name": "running_state", - "register": "0x200", - "function": "mode", - "modes": { - "0": "Standby", - "2": "Charging", - "4": "Discharging", - "6": "Fault" - } - }, - { - "name": "grid_voltage", - "register": "0x0206", - "function": "divide", - "factor": 10 - }, - { - "name": "battery_power", - "register": "0x20d", - "function": "multiply", - "factor": 10 - }, - { - "name": "grid_freq", - "register": "0x20c", - "function": "divide", - "factor": 100 - }, - { - "name": "batterySOC", - "register": "0x210" - }, - { - "name": "battery_voltage", - "register": "0x20e", - "function": "divide", - "factor": 100 - }, - { - "name": "battery_cycles", - "register": "0x22c" - }, - { - "name": "battery_temp", - "register": "0x211" - }, - { - "name": "grid_power", - "register": "0x212", - "function": "multiply", - "factor": 10 - }, - { - "name": "consumption", - "register": "0x213", - "function": "multiply", - "factor": 10 - }, - { - "name": "solarPV", - "register": "0x215", - "function": "multiply", - "factor": 10 - }, - { - "name": "today_generation", - "register": "0x218", - "function": "divide", - "factor": 100 - }, - { - "name": "today_exported", - "register": "0x219", - "function": "divide", - "factor": 100 - }, - { - "name": "today_purchase", - "register": "0x21a", - "function": "divide", - "factor": 100 - }, - { - "name": "today_consumption", - "register": "0x21b", - "function": "divide", - "factor": 100 - }, - { - "name": "inverter_temp", - "register": "0x238" - }, - { - "name": "inverterHS_temp", - "register": "0x239" - }, - { - "name": "solarPVAmps", - "register": "0x236", - "function": "divide", - "factor": 100 - }, - { - "name": "battery_current", - "register": "0x207", - "function": "divide", - "factor": 100 - } - ] + "registers": [ + { + "name": "serial_number", + "read_type": "static", + "value": "ME3000SP", + "ha": { + "name": "Serial Number", + "icon": "mdi:identifier", + "value_template": "{{ value_json.serial_number }}" + }, + "refresh": 86400 + }, + { + "name": "operating_mode", + "desc": "Operating mode control - determines charge/discharge behavior", + "type": "U16", + "function": "mode", + "modes": { + "0x0100": "Standby", + "0x0101": "Discharge", + "0x0102": "Charge", + "0x0103": "Auto" + }, + "write": true, + "mode_params": { + "0x0100": 21845, + "0x0101": 0, + "0x0102": 0, + "0x0103": 21845 + }, + "ha": { + "command_topic": "sofar/rw/operating_mode", + "entity_category": "config", + "name": "Operating Mode", + "options": [ + "Standby", + "Discharge", + "Charge", + "Auto" + ], + "value_template": "{{ value_json.operating_mode }}", + "icon": "mdi:auto-mode", + "control": "select" + } + }, + { + "name": "charge_discharge_power", + "desc": "Charge/Discharge power (0-3000W) - used with Charge/Discharge modes", + "type": "U16", + "min": 0, + "max": 3000, + "write": true, + "passive": true, + "linked_register": "operating_mode", + "ha": { + "command_topic": "sofar/rw/charge_discharge_power", + "entity_category": "config", + "name": "Charge/Discharge Power", + "min": 0, + "max": 3000, + "step": 100, + "initial": 0, + "mode": "slider", + "value_template": "{{ value_json.charge_discharge_power }}", + "icon": "mdi:battery-charging", + "unit_of_measurement": "W", + "control": "number" + } + }, + { + "name": "running_state", + "register": "0x200", + "function": "mode", + "modes": { + "0": "Standby", + "2": "Charging", + "4": "Discharging", + "6": "Fault" + }, + "ha": { + "name": "Running State", + "icon": "mdi:power-standby", + "value_template": "{{ value_json.running_state }}" + } + }, + { + "name": "model", + "value": "ME-3000SP", + "read_type": "static", + "ha": { + "name": "Model", + "icon": "mdi:tag-text", + "value_template": "{{ value_json.model }}" + }, + "refresh": 86400 + }, + { + "name": "hw_version", + "value": "TBD", + "read_type": "static", + "ha": { + "name": "HW Version", + "icon": "mdi:numeric", + "value_template": "{{ value_json.hw_version }}" + }, + "refresh": 86400 + }, + { + "name": "sw_version_com", + "read_type": "static", + "value": "TBD", + "ha": { + "name": "Software Version", + "icon": "mdi:numeric", + "value_template": "{{ value_json.sw_version_com }}" + }, + "refresh": 86400 + }, + { + "name": "fault_list_1", + "register": "0x0201", + "ha": { + "name": "Fault List 1", + "icon": "mdi:alert", + "value_template": "{{ value_json.fault_list_1 }}" + } + }, + { + "name": "fault_list_2", + "register": "0x0202", + "ha": { + "name": "Fault List 2", + "icon": "mdi:alert", + "value_template": "{{ value_json.fault_list_2 }}" + } + }, + { + "name": "fault_list_3", + "register": "0x0203", + "ha": { + "name": "Fault List 3", + "icon": "mdi:alert", + "value_template": "{{ value_json.fault_list_3 }}" + } + }, + { + "name": "fault_list_4", + "register": "0x0204", + "ha": { + "name": "Fault List 4", + "icon": "mdi:alert", + "value_template": "{{ value_json.fault_list_4 }}" + } + }, + { + "name": "fault_list_5", + "register": "0x0205", + "ha": { + "name": "Fault List 5", + "icon": "mdi:alert", + "value_template": "{{ value_json.fault_list_5 }}" + } + }, + { + "name": "grid_voltage", + "register": "0x0206", + "function": "divide", + "factor": 10, + "ha": { + "device_class": "voltage", + "unit_of_measurement": "V", + "name": "Grid Voltage", + "icon": "mdi:alpha-v-box", + "state_class": "measurement", + "value_template": "{{ value_json.grid_voltage }}" + } + }, + { + "name": "grid_current", + "register": "0x0207", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "current", + "unit_of_measurement": "A", + "name": "Grid Current", + "icon": "mdi:alpha-a-box", + "state_class": "measurement", + "value_template": "{{ value_json.grid_current }}" + } + }, + { + "name": "battery_power", + "register": "0x20d", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "power", + "unit_of_measurement": "W", + "name": "Battery Power", + "icon": "mdi:battery-charging", + "state_class": "measurement", + "value_template": "{{ value_json.battery_power }}" + } + }, + { + "name": "grid_freq", + "register": "0x20c", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "frequency", + "unit_of_measurement": "Hz", + "name": "Grid Frequency", + "icon": "mdi:sine-wave", + "state_class": "measurement", + "value_template": "{{ value_json.grid_freq }}" + } + }, + { + "name": "batterySOC", + "register": "0x210", + "ha": { + "device_class": "battery", + "unit_of_measurement": "%", + "name": "Battery SOC", + "icon": "mdi:battery-80", + "state_class": "measurement", + "value_template": "{{ value_json.batterySOC }}" + } + }, + { + "name": "battery_voltage", + "register": "0x20e", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "voltage", + "unit_of_measurement": "V", + "name": "Battery Voltage", + "icon": "mdi:alpha-v-box", + "state_class": "measurement", + "value_template": "{{ value_json.battery_voltage }}" + } + }, + { + "name": "battery_current", + "register": "0x020F", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "current", + "unit_of_measurement": "A", + "name": "Battery Current", + "icon": "mdi:alpha-a-box", + "state_class": "measurement", + "value_template": "{{ value_json.battery_current }}" + } + }, + { + "name": "battery_temp", + "register": "0x211", + "ha": { + "device_class": "temperature", + "unit_of_measurement": "°C", + "name": "Battery Temperature", + "icon": "mdi:temperature-celsius", + "state_class": "measurement", + "value_template": "{{ value_json.battery_temp }}" + } + }, + { + "name": "grid_power", + "register": "0x212", + "function": "multiply", + "factor": 10, + "ha": { + "device_class": "power", + "unit_of_measurement": "W", + "name": "Grid Power", + "icon": "mdi:lightning-bolt", + "state_class": "measurement", + "value_template": "{{ value_json.grid_power }}" + } + }, + { + "name": "consumption", + "register": "0x213", + "function": "multiply", + "factor": 10, + "ha": { + "device_class": "power", + "unit_of_measurement": "W", + "name": "Load Power", + "icon": "mdi:home-lightning-bolt-outline", + "state_class": "measurement", + "value_template": "{{ value_json.consumption }}" + } + }, + { + "name": "inverter_power", + "register": "0x0214", + "function": "multiply", + "factor": 10, + "ha": { + "device_class": "power", + "unit_of_measurement": "W", + "name": "Inverter Power", + "icon": "mdi:lightning-bolt", + "state_class": "measurement", + "value_template": "{{ value_json.inverter_power }}" + } + }, + { + "name": "solarPV", + "register": "0x215", + "function": "multiply", + "factor": 10, + "ha": { + "device_class": "power", + "unit_of_measurement": "W", + "name": "Solar PV Power", + "icon": "mdi:solar-power", + "state_class": "measurement", + "value_template": "{{ value_json.solarPV }}" + } + }, + { + "name": "eps_output_voltage", + "register": "0x0216", + "function": "divide", + "factor": 10, + "ha": { + "device_class": "voltage", + "unit_of_measurement": "V", + "name": "EPS Output Voltage", + "icon": "mdi:alpha-v-box", + "state_class": "measurement", + "value_template": "{{ value_json.eps_output_voltage }}" + } + }, + { + "name": "eps_output_power", + "register": "0x0217", + "function": "multiply", + "factor": 10, + "ha": { + "device_class": "power", + "unit_of_measurement": "W", + "name": "EPS Output Power", + "icon": "mdi:lightning-bolt", + "state_class": "measurement", + "value_template": "{{ value_json.eps_output_power }}" + } + }, + { + "name": "today_generation", + "register": "0x218", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "energy", + "unit_of_measurement": "kWh", + "name": "Solar Generation Today", + "icon": "mdi:solar-power-variant", + "state_class": "total_increasing", + "value_template": "{{ value_json.today_generation }}" + } + }, + { + "name": "today_exported", + "register": "0x219", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "energy", + "unit_of_measurement": "kWh", + "name": "Export Today", + "icon": "mdi:transmission-tower-export", + "state_class": "total_increasing", + "value_template": "{{ value_json.today_exported }}" + } + }, + { + "name": "today_purchase", + "register": "0x21a", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "energy", + "unit_of_measurement": "kWh", + "name": "Import Today", + "icon": "mdi:transmission-tower-import", + "state_class": "total_increasing", + "value_template": "{{ value_json.today_purchase }}" + } + }, + { + "name": "today_consumption", + "register": "0x21b", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "energy", + "unit_of_measurement": "kWh", + "name": "Consumption Today", + "icon": "mdi:home-lightning-bolt-outline", + "state_class": "total_increasing", + "value_template": "{{ value_json.today_consumption }}" + } + }, + { + "name": "total_generation_hi", + "register": "0x021C", + "ha": { + "name": "Total Generation Hi", + "icon": "mdi:counter", + "value_template": "{{ value_json.total_generation_hi }}" + } + }, + { + "name": "total_generation_lo", + "register": "0x021D", + "ha": { + "device_class": "energy", + "unit_of_measurement": "kWh", + "name": "Total Generation", + "icon": "mdi:solar-power-variant", + "state_class": "total_increasing", + "value_template": "{{ value_json.total_generation_lo }}" + } + }, + { + "name": "total_export_hi", + "register": "0x021E", + "ha": { + "name": "Total Export Hi", + "icon": "mdi:counter", + "value_template": "{{ value_json.total_export_hi }}" + } + }, + { + "name": "total_export_lo", + "register": "0x021F", + "ha": { + "device_class": "energy", + "unit_of_measurement": "kWh", + "name": "Total Export", + "icon": "mdi:transmission-tower-export", + "state_class": "total_increasing", + "value_template": "{{ value_json.total_export_lo }}" + } + }, + { + "name": "total_import_hi", + "register": "0x0220", + "ha": { + "name": "Total Import Hi", + "icon": "mdi:counter", + "value_template": "{{ value_json.total_import_hi }}" + } + }, + { + "name": "total_import_lo", + "register": "0x0221", + "ha": { + "device_class": "energy", + "unit_of_measurement": "kWh", + "name": "Total Import", + "icon": "mdi:transmission-tower-import", + "state_class": "total_increasing", + "value_template": "{{ value_json.total_import_lo }}" + } + }, + { + "name": "total_consumption_hi", + "register": "0x0222", + "ha": { + "name": "Total Consumption Hi", + "icon": "mdi:counter", + "value_template": "{{ value_json.total_consumption_hi }}" + } + }, + { + "name": "total_consumption_lo", + "register": "0x0223", + "ha": { + "device_class": "energy", + "unit_of_measurement": "kWh", + "name": "Total Consumption", + "icon": "mdi:home-lightning-bolt-outline", + "state_class": "total_increasing", + "value_template": "{{ value_json.total_consumption_lo }}" + } + }, + { + "name": "battery_charge_today", + "register": "0x0224", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "energy", + "unit_of_measurement": "kWh", + "name": "Battery Charge Today", + "icon": "mdi:battery-charging-80", + "state_class": "total_increasing", + "value_template": "{{ value_json.battery_charge_today }}" + } + }, + { + "name": "battery_discharge_today", + "register": "0x0225", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "energy", + "unit_of_measurement": "kWh", + "name": "Battery Discharge Today", + "icon": "mdi:battery-minus", + "state_class": "total_increasing", + "value_template": "{{ value_json.battery_discharge_today }}" + } + }, + { + "name": "battery_total_charge_hi", + "register": "0x0226", + "ha": { + "name": "Battery Total Charge Hi", + "icon": "mdi:counter", + "value_template": "{{ value_json.battery_total_charge_hi }}" + } + }, + { + "name": "battery_total_charge_lo", + "register": "0x0227", + "ha": { + "device_class": "energy", + "unit_of_measurement": "kWh", + "name": "Battery Total Charge", + "icon": "mdi:battery-charging-80", + "state_class": "total_increasing", + "value_template": "{{ value_json.battery_total_charge_lo }}" + } + }, + { + "name": "battery_total_discharge_hi", + "register": "0x0228", + "ha": { + "name": "Battery Total Discharge Hi", + "icon": "mdi:counter", + "value_template": "{{ value_json.battery_total_discharge_hi }}" + } + }, + { + "name": "battery_total_discharge_lo", + "register": "0x0229", + "ha": { + "device_class": "energy", + "unit_of_measurement": "kWh", + "name": "Battery Total Discharge", + "icon": "mdi:battery-minus", + "state_class": "total_increasing", + "value_template": "{{ value_json.battery_total_discharge_lo }}" + } + }, + { + "name": "countdown_time", + "register": "0x022A", + "ha": { + "name": "Countdown Time", + "unit_of_measurement": "s", + "icon": "mdi:timer", + "value_template": "{{ value_json.countdown_time }}" + } + }, + { + "name": "inverter_alarm_info", + "register": "0x022B", + "ha": { + "name": "Inverter Alarm Information", + "icon": "mdi:alert-circle", + "value_template": "{{ value_json.inverter_alarm_info }}" + } + }, + { + "name": "battery_cycles", + "register": "0x22c", + "ha": { + "device_class": "battery", + "unit_of_measurement": "cycles", + "name": "Battery Cycles", + "icon": "mdi:battery-sync", + "state_class": "total_increasing", + "value_template": "{{ value_json.battery_cycles }}" + } + }, + { + "name": "inverter_bus_voltage", + "register": "0x022D", + "function": "divide", + "factor": 10, + "ha": { + "device_class": "voltage", + "unit_of_measurement": "V", + "name": "Inverter Bus Voltage", + "icon": "mdi:alpha-v-box", + "state_class": "measurement", + "value_template": "{{ value_json.inverter_bus_voltage }}" + } + }, + { + "name": "llc_bus_voltage", + "register": "0x022E", + "function": "divide", + "factor": 10, + "ha": { + "device_class": "voltage", + "unit_of_measurement": "V", + "name": "LLC Bus Voltage", + "icon": "mdi:alpha-v-box", + "state_class": "measurement", + "value_template": "{{ value_json.llc_bus_voltage }}" + } + }, + { + "name": "buck_current", + "register": "0x022F", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "current", + "unit_of_measurement": "A", + "name": "Buck Current", + "icon": "mdi:alpha-a-box", + "state_class": "measurement", + "value_template": "{{ value_json.buck_current }}" + } + }, + { + "name": "battery_health_soh", + "register": "0x0237", + "ha": { + "device_class": "battery", + "unit_of_measurement": "%", + "name": "Battery Health (SOH)", + "icon": "mdi:battery-heart", + "state_class": "measurement", + "value_template": "{{ value_json.battery_health_soh }}" + } + }, + { + "name": "inverter_temp", + "register": "0x238", + "ha": { + "device_class": "temperature", + "unit_of_measurement": "°C", + "name": "Inverter Temperature", + "icon": "mdi:temperature-celsius", + "state_class": "measurement", + "value_template": "{{ value_json.inverter_temp }}" + } + }, + { + "name": "inverterHS_temp", + "register": "0x239", + "ha": { + "device_class": "temperature", + "unit_of_measurement": "°C", + "name": "Heatsink Temperature", + "icon": "mdi:temperature-celsius", + "state_class": "measurement", + "value_template": "{{ value_json.inverterHS_temp }}" + } + }, + { + "name": "solarPVAmps", + "register": "0x236", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "current", + "unit_of_measurement": "A", + "name": "Solar PV Current", + "icon": "mdi:alpha-a-box", + "state_class": "measurement", + "value_template": "{{ value_json.solarPVAmps }}" + } + }, + { + "name": "battery_current", + "register": "0x207", + "function": "divide", + "factor": 100, + "ha": { + "device_class": "current", + "unit_of_measurement": "A", + "name": "Battery Current", + "icon": "mdi:alpha-a-box", + "state_class": "measurement", + "value_template": "{{ value_json.battery_current }}" + } + } + ] } \ No newline at end of file diff --git a/sofar2mqtt-v2.py b/sofar2mqtt-v2.py index 203330a..c6b1950 100644 --- a/sofar2mqtt-v2.py +++ b/sofar2mqtt-v2.py @@ -16,6 +16,7 @@ import paho.mqtt.client as mqtt import requests + def load_config(config_file_path): """ Load configuration file """ config = {} @@ -24,6 +25,8 @@ def load_config(config_file_path): return config # pylint: disable=too-many-instance-attributes + + class Sofar(): """ Sofar """ @@ -59,22 +62,27 @@ def __init__(self, config_file_path, daemon, retry, retry_delay, write_retry, wr self.legacy_publish = legacy_publish self.data = {} self.log_level = logging.getLevelName(log_level) - logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.getLevelName(log_level)) + logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', + level=logging.getLevelName(log_level)) self.mutex = threading.Lock() - self.client = mqtt.Client(client_id=f"sofar2mqtt-{socket.gethostname()}", userdata=None, protocol=mqtt.MQTTv5, transport="tcp") + self.client = mqtt.Client( + client_id=f"sofar2mqtt-{socket.gethostname()}", userdata=None, protocol=mqtt.MQTTv5, transport="tcp") self.setup_mqtt(logging) self.setup_instrument() self.iteration = 0 - + def on_connect(self, client, userdata, flags, rc, properties=None): logging.info("MQTT "+mqtt.connack_string(rc)) if rc == 0: try: logging.info(f"Subscribing to homeassistant/status") - client.subscribe(f"homeassistant/status", qos=0, options=None, properties=None) + client.subscribe(f"homeassistant/status", qos=0, + options=None, properties=None) for register in self.write_registers: - logging.info(f"Subscribing to {self.write_topic}/{register['name']}") - client.subscribe(f"{self.write_topic}/{register['name']}", qos=0, options=None, properties=None) + logging.info( + f"Subscribing to {self.write_topic}/{register['name']}") + client.subscribe( + f"{self.write_topic}/{register['name']}", qos=0, options=None, properties=None) except Exception: logging.info(traceback.format_exc()) @@ -83,10 +91,13 @@ def on_disconnect(client, userdata, rc, properties=None): logging.info("MQTT un-expected disconnect") def on_message(self, client, userdata, message, properties=None): + if message.retain: + logging.info(f"Ignoring retained message on topic {message.topic}") + return found = False valid = False topic = message.topic - payload = message.payload.decode("utf-8") + payload = message.payload.decode("utf-8") if topic == "homeassistant/status": logging.info(f"Received message for {topic}:{payload}") if payload == "online": @@ -98,57 +109,75 @@ def on_message(self, client, userdata, message, properties=None): found = True if 'function' in register: if register['function'] == 'mode': - new_mode = False + new_mode = None for key in register['modes']: if register['modes'][key] == payload: - new_mode = key - logging.info(f"Received a request for {register['name']} to set mode value to: {payload}({new_mode})") + new_mode = key + logging.info( + f"Received a request for {register['name']} to set mode value to: {payload}({new_mode})") if not new_mode: - logging.error(f"Received a request for {register['name']} but mode value: {payload} is not a known mode. Ignoring") + logging.error( + f"Received a request for {register['name']} but mode value: {payload} is not a known mode. Ignoring") if register['name'] in self.data: retry = self.write_retry while retry > 0: - if self.data[register['name']] == payload: - logging.info(f"Current value for {register['name']}={self.data[register['name']]} matches desired value: {payload}. Ignoring") + if register['name'] in self.data and self.data[register['name']] == payload: + logging.info( + f"Current value for {register['name']}={self.data[register['name']]} matches desired value: {payload}. Ignoring") retry = 0 else: - logging.info(f"Current value for {register['name']}={self.data[register['name']]}, attempting to set it to: {payload}. Retries remaining: {retry}") - self.write_value(register, int(new_mode)) + logging.info( + f"Current value for {register['name']}={self.data.get(register['name'], 'unknown')}, attempting to set it to: {payload}. Retries remaining: {retry}") + + # Convert hex string to int if needed + if isinstance(new_mode, str) and new_mode.startswith('0x'): + write_value = int(new_mode, 16) + else: + write_value = int(new_mode) + self.write_value(register, write_value) + time.sleep(self.write_retry_delay) retry = retry - 1 - else: + else: logging.error(f"No current read value for {register['name']} skipping write operation. Please try again.") elif register['function'] == 'int': value = int(payload) - logging.info(f"Received a request for {register['name']} to set value to: {payload}({value})") + logging.info( + f"Received a request for {register['name']} to set value to: {payload}({value})") if value < register['min']: - logging.error(f"Received a request for {register['name']} but value: {value} is less than the min value: {register['min']}. Ignoring") + logging.error( + f"Received a request for {register['name']} but value: {value} is less than the min value: {register['min']}. Ignoring") elif value > register['max']: - logging.error(f"Received a request for {register['name']} but value: {value} is more than the max value: {register['max']}. Ignoring") + logging.error( + f"Received a request for {register['name']} but value: {value} is more than the max value: {register['max']}. Ignoring") else: if register['name'] == 'desired_power': - if 'energy_storage_mode' in self.data: + if 'energy_storage_mode' in self.data: if 'Passive mode' != self.data['energy_storage_mode']: logging.info(f"Received a request for {register['name']} but not not in Passive mode. Ignoring") continue + if register['name'] in self.data: retry = self.write_retry while retry > 0: if self.data[register['name']] == value: - logging.info(f"Current value for {register['name']}={self.data[register['name']]} matches desired value: {value}. Ignoring") + logging.info( + f"Current value for {register['name']}={self.data[register['name']]} matches desired value: {value}. Ignoring") retry = 0 else: - logging.info(f"Current value for {register['name']}={self.data[register['name']]}, attempting to set it to {value}. Retries remaining: {retry}") + logging.info( + f"Current value for {register['name']}={self.data[register['name']]}, attempting to set it to {value}. Retries remaining: {retry}") self.write_value(register, value) time.sleep(self.write_retry_delay) retry = retry - 1 - else: - logging.error(f"No current read value for {register['name']} skipping write operation. Please try again.") + else: + logging.error( + f"No current read value for {register['name']} skipping write operation. Please try again.") if not found: - logging.error(f"Received a request to set an unknown register: {register_name['name']} to {payload}") - + logging.error( + f"Received a request to set an unknown register") def setup_mqtt(self, logging): self.client.enable_logger(logger=logging) @@ -156,11 +185,14 @@ def setup_mqtt(self, logging): self.client.on_message = self.on_message if self.username is not None and self.password is not None: self.client.username_pw_set(self.username, self.password) - logging.info(f"MQTT connecting to broker {self.broker} port {self.port} with auth user {self.username}") + logging.info( + f"MQTT connecting to broker {self.broker} port {self.port} with auth user {self.username}") else: - logging.info(f"MQTT connecting to broker {self.broker} port {self.port} without auth") + logging.info( + f"MQTT connecting to broker {self.broker} port {self.port} without auth") self.client.reconnect_delay_set(min_delay=1, max_delay=300) - self.client.connect(self.broker, port=self.port, keepalive=60, bind_address="", bind_port=0, clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, properties=None) + self.client.connect(self.broker, port=self.port, keepalive=60, bind_address="", + bind_port=0, clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY, properties=None) self.client.loop_start() def setup_instrument(self): @@ -171,14 +203,14 @@ def setup_instrument(self): self.instrument.serial.bytesize = 8 self.instrument.serial.parity = serial.PARITY_NONE self.instrument.serial.stopbits = 1 - self.instrument.serial.timeout = 0.2 # seconds + self.instrument.serial.timeout = 0.2 # seconds self.instrument.close_port_after_each_call = True def read_and_publish(self): for register in self.config['registers']: refresh = 1 if 'refresh' in register: - refresh = register['refresh'] + refresh = register['refresh'] if (self.iteration % refresh) != 0: logging.debug(f"Skipping {register['name']}") continue @@ -199,13 +231,14 @@ def read_and_publish(self): elif register['agg_function'] == 'subtract': value -= self.data[register_name] elif register['agg_function'] == 'avg': - value = int((value + self.data[register_name]) / 2) - if 'invert' in register: - if register['invert']: - if value > 0: - value = -abs(value) - else: - value = abs(value) + value = int( + (value + self.data[register_name]) / 2) + if 'invert' in register: + if register['invert']: + if value > 0: + value = -abs(value) + else: + value = abs(value) else: read_type = 'register' registers = 1 @@ -213,12 +246,16 @@ def read_and_publish(self): read_type = register['read_type'] if 'registers' in register: registers = register['registers'] - value = self.read_value( - int(register['register'], 16), - read_type, - signed, - registers - ) + if read_type == 'static': + value = register['value'] + else: + if 'register' in register: + value = self.read_value( + int(register['register'], 16), + read_type, + signed, + registers + ) if value is None: continue else: @@ -227,11 +264,13 @@ def read_and_publish(self): value = 0 if 'min' in register: if value < register['min']: - logging.error(f"Value for {register['name']}: {str(value)} is lower than min allowed value: {register['min']}. Ignoring value") + logging.error( + f"Value for {register['name']}: {str(value)} is lower than min allowed value: {register['min']}. Ignoring value") continue if 'max' in register: if value > register['max']: - logging.error(f"Value for {register['name']}: {str(value)} is greater than max allowed value: {register['max']}. Ignoring value") + logging.error( + f"Value for {register['name']}: {str(value)} is greater than max allowed value: {register['max']}. Ignoring value") continue if 'function' in register: if register['function'] == 'multiply': @@ -242,7 +281,8 @@ def read_and_publish(self): try: value = register['modes'][str(value)] except KeyError: - logging.error(f"Unknown mode value for {register['name']} value: {str(value)}") + logging.error( + f"Unknown mode value for {register['name']} value: {str(value)}") elif register['function'] == 'bit_field': length = len(register['fields']) fields = [] @@ -251,16 +291,19 @@ def read_and_publish(self): fields.append(register['fields'][n]) value = (','.join(fields)) elif register['function'] == 'high_bit_low_bit': - high = value >> 8 # shift right - low = value & 255 # apply bitmask - value = f"{high:02}{register['join']}{low:02}" # combine and pad 2 zeros + high = value >> 8 # shift right + low = value & 255 # apply bitmask + # combine and pad 2 zeros + value = f"{high:02}{register['join']}{low:02}" logging.debug('Read %s:%s', register['name'], value) self.publish(register['name'], value) - failure_percentage = round(self.failures / (self.requests+self.retries)*100,2) - retry_percentage = round(self.retries / (self.requests)*100,2) - logging.info(f"Modbus Requests: {self.requests} Retries: {self.retries} ({retry_percentage}%) Failures: {self.failures} ({failure_percentage}%)") + failure_percentage = round( + self.failures / (self.requests+self.retries)*100, 2) + retry_percentage = round(self.retries / (self.requests)*100, 2) + logging.info( + f"Modbus Requests: {self.requests} Retries: {self.retries} ({retry_percentage}%) Failures: {self.failures} ({failure_percentage}%)") self.data['modbus_failures'] = self.failures self.data['modbus_requests'] = self.requests self.data['modbus_retries'] = self.retries @@ -282,7 +325,8 @@ def read(self): def publish_state(self): try: data = json.dumps(self.data, indent=2) - self.client.publish("sofar2mqtt_python/bridge", "online", retain=False) + self.client.publish("sofar2mqtt_python/bridge", + "online", retain=False) self.client.publish(self.topic + "state_all", data, retain=True) with open("data.json", "w") as write_file: write_file.write(data) @@ -292,17 +336,18 @@ def publish_state(self): def publish_mqtt_discovery(self): if 'serial_number' not in self.data: - logging.error("Serial number could not be determined, skipping publish") + logging.error( + "Serial number could not be determined, skipping publish") return False sn = self.data['serial_number'] payload = { "device": { - "identifiers": [f"sofar2mqtt_python_bridge_{sn}"], - "manufacturer": "Sofar2Mqtt-Python", - "model": "Bridge", - "name": "Sofar2Mqtt Python Bridge", - "sw_version": "3.0.3" + "identifiers": [f"sofar2mqtt_python_bridge_{sn}"], + "manufacturer": "Sofar2Mqtt-Python", + "model": "Bridge", + "name": "Sofar2Mqtt Python Bridge", + "sw_version": "3.0.6" }, "device_class": "connectivity", "entity_category": "diagnostic", @@ -335,7 +380,7 @@ def publish_mqtt_discovery(self): "sw_version": self.data["sw_version_com"], "hw_version": self.data["hw_version"], "manufacturer": "SOFAR", - "model": "HYD-6000-EP", + "model": self.data["model"], "configuration_url": "https://github.com/rjpearce/sofar2mqtt-python", "identifiers": [f"{sn}"] }, @@ -343,7 +388,7 @@ def publish_mqtt_discovery(self): { "topic": "sofar2mqtt_python/bridge", "value": "online" - } + } ], } payload = default_payload | register['ha'] @@ -356,36 +401,38 @@ def publish_mqtt_discovery(self): logging.info(traceback.format_exc()) def signal_handler(self, sig, _frame): - logging.info(f"Received signal {sig}, attempting to stop") - self.daemon = False + logging.info(f"Received signal {sig}, attempting to stop") + self.daemon = False def terminate(self): - logging.info("Terminating") - logging.info(f"Publishing offline to sofar2mqtt_python/bridge") - self.client.publish("sofar2mqtt_python/bridge", "offline", retain=False) - self.client.loop_stop() - exit(0) + logging.info("Terminating") + logging.info(f"Publishing offline to sofar2mqtt_python/bridge") + self.client.publish("sofar2mqtt_python/bridge", + "offline", retain=False) + self.client.loop_stop() + exit(0) def main(self): """ Main method """ signal.signal(signal.SIGTERM, self.signal_handler) signal.signal(signal.SIGINT, self.signal_handler) if not self.daemon: - self.read_and_publish() + self.read_and_publish() while (self.daemon): self.read() if self.iteration == 0: self.publish_mqtt_discovery() self.publish_state() time.sleep(self.refresh_interval) - self.iteration+=1 + self.iteration += 1 self.terminate() def publish(self, key, value): if key == 'energy_storage_mode': if key in self.data: if value != self.data[key]: - logging.info(f"energy_storage_mode has changed to: {value}") + logging.info( + f"energy_storage_mode has changed to: {value}") self.data[key] = value if self.legacy_publish: logging.debug('Publishing %s:%s', self.topic + key, value) @@ -395,48 +442,63 @@ def publish(self, key, value): logging.debug(traceback.format_exc()) def write_value(self, register, value): - """ Read value from register with a retry mechanism """ + """ Write value to register with a retry mechanism """ with self.mutex: retry = self.write_retry - logging.info(f"Writing {register['register']}({int(register['register'], 16)}) with {value}({value})") + logging.info( + f"Writing {register['name']} register with value {value}") signed = False success = False retries = 0 - failed = 0 + failed = 0 if 'signed' in register: signed = register['signed'] while retry > 0 and not success: try: - if register['type'] == 'U16': - self.instrument.write_register(int(register['register'],16), int(value)) - elif register['type'] == 'I32': + if 'type' in register: + reg_type = register['type'] + else: + reg_type = 'U16' + + if reg_type == 'U16': + self.instrument.write_register( + int(register['register'], 16), int(value)) + elif reg_type == 'I32': # split the value to a byte values = struct.pack(">l", value) # split low and high byte - low = struct.unpack(">H", bytearray([values[0], values[1]]))[0] - high = struct.unpack(">H", bytearray([values[2], values[3]]))[0] + low = struct.unpack(">H", bytearray( + [values[0], values[1]]))[0] + high = struct.unpack( + ">H", bytearray([values[2], values[3]]))[0] # send the registers - self.instrument.write_registers(int(register['register'],16), [0, 0, low, high, low, high]) + self.instrument.write_registers(int(register['register'], 16), [ + 0, 0, low, high, low, high]) except minimalmodbus.NoResponseError: - logging.debug(f"Failed to write_register {register['name']} {traceback.format_exc()}") + logging.debug( + f"Failed to write_register {register['name']} {traceback.format_exc()}") retry = retry - 1 retries = retries + 1 time.sleep(self.write_retry_delay) except minimalmodbus.InvalidResponseError: - logging.debug(f"Failed to write_register {register['name']} {traceback.format_exc()}") + logging.debug( + f"Failed to write_register {register['name']} {traceback.format_exc()}") retry = retry - 1 retries = retries + 1 time.sleep(self.write_retry_delay) except serial.serialutil.SerialException: - logging.debug(f"Failed to write_register {register['name']} {traceback.format_exc()}") + logging.debug( + f"Failed to write_register {register['name']} {traceback.format_exc()}") retry = retry - 1 retries = retries + 1 time.sleep(self.write_retry_delay) success = True if success: - logging.info('Modbus Write Request: %s successful. Retries: %d', register['name'], retries) + logging.info( + 'Modbus Write Request: %s successful. Retries: %d', register['name'], retries) else: - logging.error('Modbus Write Request: %s failed. Retry exhausted. Retries: %d', register['name'], retries) + logging.error( + 'Modbus Write Request: %s failed. Retry exhausted. Retries: %d', register['name'], retries) def read_value(self, registeraddress, read_type, signed, registers=1): """ Read value from register with a retry mechanism """ @@ -445,7 +507,7 @@ def read_value(self, registeraddress, read_type, signed, registers=1): retry = self.retry while retry > 0 and value is None: try: - self.requests +=1 + self.requests += 1 if read_type == "register": value = self.instrument.read_register( registeraddress, 0, functioncode=3, signed=signed) @@ -476,6 +538,7 @@ def read_value(self, registeraddress, read_type, signed, registers=1): self.failed.append(registeraddress) return value + @click.command("cli", context_settings={'show_default': True}) @click.option( '--config-file', @@ -584,9 +647,11 @@ def read_value(self, registeraddress, read_type, signed, registers=1): # pylint: disable=too-many-arguments def main(config_file, daemon, retry, retry_delay, write_retry, write_retry_delay, refresh_interval, broker, port, username, password, topic, write_topic, log_level, device, legacy_publish): """Main""" - sofar = Sofar(config_file, daemon, retry, retry_delay, write_retry, write_retry_delay, refresh_interval, broker, port, username, password, topic, write_topic, log_level, device, legacy_publish) + sofar = Sofar(config_file, daemon, retry, retry_delay, write_retry, write_retry_delay, refresh_interval, + broker, port, username, password, topic, write_topic, log_level, device, legacy_publish) sofar.main() + # pylint: disable=no-value-for-parameter if __name__ == '__main__': main()