From 082de2152bdb082e4163f5265ca4c4a694289523 Mon Sep 17 00:00:00 2001 From: maslyankov Date: Thu, 26 Dec 2024 23:57:13 +0200 Subject: [PATCH 1/7] Fix "Grid Charge Enabled" on value --- src/sunsynk/definitions/three_phase_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sunsynk/definitions/three_phase_common.py b/src/sunsynk/definitions/three_phase_common.py index 1c3f74ce..103ed684 100644 --- a/src/sunsynk/definitions/three_phase_common.py +++ b/src/sunsynk/definitions/three_phase_common.py @@ -192,7 +192,7 @@ SENSORS += ( NumberRWSensor(128, "Grid Charge Battery current", AMPS, max=240), NumberRWSensor(127, "Grid Charge Start Battery SOC", "%"), - SwitchRWSensor(130, "Grid Charge enabled"), + SwitchRWSensor(130, "Grid Charge enabled", on=1), SwitchRWSensor(146, "Use Timer"), SwitchRWSensor(145, "Solar Export"), NumberRWSensor(143, "Export Limit power", WATT, max=RATED_POWER), From 8e5ca3ba44d189a8f3158196075a05a1265e4376 Mon Sep 17 00:00:00 2001 From: maslyankov Date: Thu, 26 Dec 2024 23:57:32 +0200 Subject: [PATCH 2/7] Make sensor dependencies handle recursive --- src/ha_addon_sunsynk_multi/sensor_options.py | 44 +++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/ha_addon_sunsynk_multi/sensor_options.py b/src/ha_addon_sunsynk_multi/sensor_options.py index ac0ca5e2..e0bc779b 100644 --- a/src/ha_addon_sunsynk_multi/sensor_options.py +++ b/src/ha_addon_sunsynk_multi/sensor_options.py @@ -46,6 +46,21 @@ class SensorOptions(dict[Sensor, SensorOption]): startup: set[Sensor] = attrs.field(factory=set) + def _add_sensor_with_deps(self, sensor: Sensor, visible: bool = False) -> None: + """Add a sensor and all its dependencies recursively.""" + if sensor not in self: + self[sensor] = SensorOption( + sensor=sensor, + schedule=get_schedule(sensor, SCHEDULES), + visible=visible, + ) + + if isinstance(sensor, RWSensor): + for dep in sensor.dependencies: + self.startup.add(dep) + self._add_sensor_with_deps(dep, visible=False) # Recursive call + self[dep].affects.add(sensor) + def init_sensors(self) -> None: """Parse options and get the various sensor lists.""" if not DEFS.all: @@ -54,22 +69,12 @@ def init_sensors(self) -> None: # Add startup sensors self.startup = {DEFS.rated_power, DEFS.serial} - self[DEFS.rated_power] = SensorOption( - sensor=DEFS.rated_power, schedule=Schedule(), visible=False - ) - self[DEFS.serial] = SensorOption( - sensor=DEFS.serial, schedule=Schedule(), visible=False - ) + self._add_sensor_with_deps(DEFS.rated_power, visible=False) + self._add_sensor_with_deps(DEFS.serial, visible=False) # Add sensors from config for sen in get_sensors(target=self, names=OPT.sensors): - if sen in self: - continue - self[sen] = SensorOption( - sensor=sen, - schedule=get_schedule(sen, SCHEDULES), - visible=True, - ) + self._add_sensor_with_deps(sen, visible=True) # Add 1st inverter sensors for sen in get_sensors(target=self, names=OPT.sensors_first_inverter): @@ -80,18 +85,7 @@ def init_sensors(self) -> None: visible=True, first=True, ) - - # Handle RW sensor deps - for sopt in list(self.values()): - if isinstance(sopt.sensor, RWSensor): - for dep in sopt.sensor.dependencies: - self.startup.add(dep) - if dep not in self: - self[dep] = SensorOption( - sensor=dep, - schedule=get_schedule(dep, SCHEDULES), - ) - self[dep].affects.add(sopt.sensor) + self._add_sensor_with_deps(sen, visible=True) # Info if we have hidden sensors if hidden := [s.sensor.name for s in self.values() if not s.visible]: From a128925bf47e4b06604482621af4553ceafa583e Mon Sep 17 00:00:00 2001 From: maslyankov Date: Fri, 27 Dec 2024 00:11:32 +0200 Subject: [PATCH 3/7] Handle circular dependency in deps recursion --- src/ha_addon_sunsynk_multi/sensor_options.py | 23 +++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/ha_addon_sunsynk_multi/sensor_options.py b/src/ha_addon_sunsynk_multi/sensor_options.py index e0bc779b..4b7bd650 100644 --- a/src/ha_addon_sunsynk_multi/sensor_options.py +++ b/src/ha_addon_sunsynk_multi/sensor_options.py @@ -46,8 +46,23 @@ class SensorOptions(dict[Sensor, SensorOption]): startup: set[Sensor] = attrs.field(factory=set) - def _add_sensor_with_deps(self, sensor: Sensor, visible: bool = False) -> None: - """Add a sensor and all its dependencies recursively.""" + def _add_sensor_with_deps(self, sensor: Sensor, visible: bool = False, path: set[Sensor] | None = None) -> None: + """Add a sensor and all its dependencies recursively. + + Args: + sensor: The sensor to add + visible: Whether the sensor should be visible + path: Set of sensors in the current dependency path to detect cycles + """ + if path is None: + path = set() + + if sensor in path: + _LOGGER.warning("Circular dependency detected for sensor %s", sensor.name) + return + + path.add(sensor) + if sensor not in self: self[sensor] = SensorOption( sensor=sensor, @@ -58,8 +73,10 @@ def _add_sensor_with_deps(self, sensor: Sensor, visible: bool = False) -> None: if isinstance(sensor, RWSensor): for dep in sensor.dependencies: self.startup.add(dep) - self._add_sensor_with_deps(dep, visible=False) # Recursive call + self._add_sensor_with_deps(dep, visible=False, path=path.copy()) # Pass copy of path self[dep].affects.add(sensor) + + path.remove(sensor) def init_sensors(self) -> None: """Parse options and get the various sensor lists.""" From 10f5d4a8f313062eda21e50e34a7878ddccfa095 Mon Sep 17 00:00:00 2001 From: maslyankov Date: Fri, 27 Dec 2024 00:51:55 +0200 Subject: [PATCH 4/7] Improve sensor groups, add new ones and update/improve definitions docs --- src/ha_addon_sunsynk_multi/sensor_options.py | 61 ++++++++ www/docs/reference/definitions.md | 146 ++++++++++++++++++- 2 files changed, 206 insertions(+), 1 deletion(-) diff --git a/src/ha_addon_sunsynk_multi/sensor_options.py b/src/ha_addon_sunsynk_multi/sensor_options.py index 4b7bd650..3c7a3bb5 100644 --- a/src/ha_addon_sunsynk_multi/sensor_options.py +++ b/src/ha_addon_sunsynk_multi/sensor_options.py @@ -181,9 +181,14 @@ def import_definitions() -> None: "inverter_current", "inverter_power", "load_frequency", + "load_power", + "load_l1_power", + "load_l2_power", + "load_l3_power", "non_essential_power", "overall_state", "priority_load", + "pv_power", "pv1_current", "pv1_power", "pv1_voltage", @@ -218,6 +223,62 @@ def import_definitions() -> None: "prog6_charge", "prog6_power", "prog6_time", + "date_time", + "grid_charge_battery_current", + "grid_charge_start_battery_soc", + "grid_charge_enabled", + "use_timer", + "solar_export", + "export_limit_power", + "battery_max_charge_current", + "battery_max_discharge_current", + "battery_capacity_current", + "battery_shutdown_capacity", + "battery_restart_capacity", + "battery_low_capacity", + "battery_type", + "battery_wake_up", + "battery_resistance", + "battery_charge_efficiency", + "grid_standard", + "configured_grid_frequency", + "configured_grid_phases", + "ups_delay_time", + ], + "generator": [ + "generator_port_usage", + "generator_off_soc", + "generator_on_soc", + "generator_max_operating_time", + "generator_cooling_time", + "min_pv_power_for_gen_start", + "generator_charge_enabled", + "generator_charge_start_battery_soc", + "generator_charge_battery_current", + "gen_signal_on", + ], + "diagnostics": [ + "grid_voltage", + "grid_l1_voltage", + "grid_l2_voltage", + "grid_l3_voltage", + "battery_temperature", + "battery_voltage", + "battery_soc", + "battery_power", + "battery_current", + "fault", + "dc_transformer_temperature", + "radiator_temperature", + "grid_relay_status", + "inverter_relay_status", + "battery_bms_alarm_flag", + "battery_bms_fault_flag", + "battery_bms_soh", + "fan_warning", + "grid_phase_warning", + "lithium_battery_loss_warning", + "parallel_communication_quality_warning", ], } diff --git a/www/docs/reference/definitions.md b/www/docs/reference/definitions.md index 17dc80a9..393ef3dc 100644 --- a/www/docs/reference/definitions.md +++ b/www/docs/reference/definitions.md @@ -166,13 +166,157 @@ prog6_capacity prog6_charge prog6_power prog6_time +date_time +grid_charge_battery_current +grid_charge_start_battery_soc +grid_charge_enabled +use_timer +solar_export +export_limit_power +battery_max_charge_current +battery_max_discharge_current +battery_capacity_current +battery_shutdown_capacity +battery_restart_capacity +battery_low_capacity +battery_type +battery_wake_up +battery_resistance +battery_charge_efficiency +grid_standard +configured_grid_frequency +configured_grid_phases +ups_delay_time +``` + +::: + +### Generator + +Sensors used for generator control and monitoring. + +```yaml +SENSORS: + - generator +``` + +::: details Sensors included + +```yaml +generator_port_usage +generator_off_soc +generator_on_soc +generator_max_operating_time +generator_cooling_time +min_pv_power_for_gen_start +generator_charge_enabled +generator_charge_start_battery_soc +generator_charge_battery_current +gen_signal_on +``` + +::: + +### Diagnostics + +Sensors used for system diagnostics and monitoring. + +```yaml +SENSORS: + - diagnostics +``` + +::: details Sensors included + +```yaml +grid_voltage +grid_l1_voltage +grid_l2_voltage +grid_l3_voltage +battery_temperature +battery_voltage +battery_soc +battery_power +battery_current +fault +dc_transformer_temperature +radiator_temperature +grid_relay_status +inverter_relay_status +battery_bms_alarm_flag +battery_bms_fault_flag +battery_bms_soh +fan_warning +grid_phase_warning +lithium_battery_loss_warning +parallel_communication_quality_warning ``` ::: ### My Sensors -All your [custom sensors](mysensors) can be added to the configuration using the `mysensors` group. +You can create custom sensors by defining them in a file called `mysensors.py` in the `/share/hass-addon-sunsynk/` directory. This allows you to add sensors that are not included in the default definitions. + +To create custom sensors: + +1. Create the directory and file: + ```bash + /share/hass-addon-sunsynk/mysensors.py + ``` + +2. Define your sensors in the file. Here's a basic example: + ```python + from sunsynk import AMPS, CELSIUS, KWH, VOLT, WATT + from sunsynk.rwsensors import NumberRWSensor, SelectRWSensor + from sunsynk.sensors import Sensor, SensorDefinitions, MathSensor + + # Initialize the sensor definitions + SENSORS = SensorDefinitions() + + # Add your custom sensors + SENSORS += ( + # Basic sensor example + Sensor(178, "My Custom Power Sensor", WATT, -1), + + # Math sensor example (combining multiple registers) + MathSensor((175, 172), "Custom Combined Power", WATT, factors=(1, 1)), + + # Read/Write sensor example + NumberRWSensor(130, "Custom Control Setting", "%", min=0, max=100), + ) + ``` + +3. Add your custom sensors to your configuration using either individual sensors or the `mysensors` group: + ```yaml + SENSORS: + - mysensors # Adds all custom sensors + # Or add specific sensors: + - my_custom_power_sensor + - custom_combined_power + - custom_control_setting + ``` + +The sensor definition parameters are: +- First parameter: Register number(s) +- Second parameter: Sensor name +- Third parameter: Unit (WATT, VOLT, AMPS, etc.) +- Last parameter: Scale factor (optional) + +You can create different types of sensors: +- `Sensor`: Basic read-only sensor +- `MathSensor`: Combines multiple registers with mathematical operations +- `NumberRWSensor`: Read/write sensor for configurable values +- `SelectRWSensor`: Read/write sensor with predefined options +- `SwitchRWSensor`: Read/write sensor for boolean values + +Once defined, your custom sensors will be loaded automatically when the addon starts, and you'll see them listed in the startup logs: +```log +INFO Importing /share/hass-addon-sunsynk/mysensors.py... +INFO custom sensors: my_custom_power_sensor, custom_combined_power, custom_control_setting +``` + +All your [custom sensors](mysensors) can be added to the configuration using the `mysensors` group: ```yaml SENSORS: From 86909909778fcc317b5aa8fe6c902d595f191c42 Mon Sep 17 00:00:00 2001 From: maslyankov Date: Fri, 27 Dec 2024 01:28:53 +0200 Subject: [PATCH 5/7] Fix pylint trailing spaces --- src/ha_addon_sunsynk_multi/sensor_options.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ha_addon_sunsynk_multi/sensor_options.py b/src/ha_addon_sunsynk_multi/sensor_options.py index 3c7a3bb5..32e84a5c 100644 --- a/src/ha_addon_sunsynk_multi/sensor_options.py +++ b/src/ha_addon_sunsynk_multi/sensor_options.py @@ -56,26 +56,26 @@ def _add_sensor_with_deps(self, sensor: Sensor, visible: bool = False, path: set """ if path is None: path = set() - + if sensor in path: _LOGGER.warning("Circular dependency detected for sensor %s", sensor.name) return - + path.add(sensor) - + if sensor not in self: self[sensor] = SensorOption( sensor=sensor, schedule=get_schedule(sensor, SCHEDULES), visible=visible, ) - + if isinstance(sensor, RWSensor): for dep in sensor.dependencies: self.startup.add(dep) self._add_sensor_with_deps(dep, visible=False, path=path.copy()) # Pass copy of path self[dep].affects.add(sensor) - + path.remove(sensor) def init_sensors(self) -> None: From 60a4929adbcf76acc1c9f3aa9047f500f1bf8e56 Mon Sep 17 00:00:00 2001 From: maslyankov Date: Fri, 27 Dec 2024 01:38:02 +0200 Subject: [PATCH 6/7] Update tests with all prog times --- src/tests/ha_addon_sunsynk_multi/test_sensors.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tests/ha_addon_sunsynk_multi/test_sensors.py b/src/tests/ha_addon_sunsynk_multi/test_sensors.py index 87c46f19..1b261049 100644 --- a/src/tests/ha_addon_sunsynk_multi/test_sensors.py +++ b/src/tests/ha_addon_sunsynk_multi/test_sensors.py @@ -16,7 +16,11 @@ def test_opt1() -> None: OPT.sensors = ["prog1_time"] SOPT.init_sensors() assert sorted(s.id for s in SOPT.startup) == [ + "prog1_time", "prog2_time", + "prog3_time", + "prog4_time", + "prog5_time", "prog6_time", "rated_power", "serial", From 912551541c14fe1e63317539ec2781d40fe8fb14 Mon Sep 17 00:00:00 2001 From: maslyankov Date: Fri, 27 Dec 2024 01:45:03 +0200 Subject: [PATCH 7/7] Refactor sensor addition logic in SensorOptions to improve handling of visibility and dependencies. Ensure sensors are added to startup set regardless of visibility, and only include them in SOPT if explicitly requested or if they are direct dependencies. Enhance dependency tracking to avoid unnecessary affects tracking. --- src/ha_addon_sunsynk_multi/sensor_options.py | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/ha_addon_sunsynk_multi/sensor_options.py b/src/ha_addon_sunsynk_multi/sensor_options.py index 32e84a5c..f8591057 100644 --- a/src/ha_addon_sunsynk_multi/sensor_options.py +++ b/src/ha_addon_sunsynk_multi/sensor_options.py @@ -63,18 +63,23 @@ def _add_sensor_with_deps(self, sensor: Sensor, visible: bool = False, path: set path.add(sensor) - if sensor not in self: - self[sensor] = SensorOption( - sensor=sensor, - schedule=get_schedule(sensor, SCHEDULES), - visible=visible, - ) + # Add to startup set regardless of visibility + self.startup.add(sensor) + + # Only add to SOPT if it's explicitly requested (visible) or a direct dependency + if visible or len(path) <= 2: # Original sensor or direct dependency + if sensor not in self: + self[sensor] = SensorOption( + sensor=sensor, + schedule=get_schedule(sensor, SCHEDULES), + visible=visible, + ) if isinstance(sensor, RWSensor): for dep in sensor.dependencies: - self.startup.add(dep) self._add_sensor_with_deps(dep, visible=False, path=path.copy()) # Pass copy of path - self[dep].affects.add(sensor) + if dep in self and sensor in self: # Only track affects if both sensors are in SOPT + self[dep].affects.add(sensor) path.remove(sensor)