Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvement/recursive handle sensor dependencies #377

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 102 additions & 25 deletions src/ha_addon_sunsynk_multi/sensor_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,43 @@

startup: set[Sensor] = attrs.field(factory=set)

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)

# 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._add_sensor_with_deps(dep, visible=False, path=path.copy()) # Pass copy of path
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)

def init_sensors(self) -> None:
"""Parse options and get the various sensor lists."""
if not DEFS.all:
Expand All @@ -54,22 +91,12 @@

# 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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add rated_power or serial in your config?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(or will visible always be false?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it so that all dependencies that are not defined in the config are hidden by default. There are further improvements to this branch on other branches, but my repo has diverged from this one somewhat.

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):
Expand All @@ -80,18 +107,7 @@
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)

Check warning on line 110 in src/ha_addon_sunsynk_multi/sensor_options.py

View check run for this annotation

Codecov / codecov/patch

src/ha_addon_sunsynk_multi/sensor_options.py#L110

Added line #L110 was not covered by tests
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need a first=True here as well? (passed to the SensorOptions?) - #374 made me think of this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this change because of failing tests. Will look into it if it is necessary.


# Info if we have hidden sensors
if hidden := [s.sensor.name for s in self.values() if not s.visible]:
Expand Down Expand Up @@ -170,9 +186,14 @@
"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",
Expand Down Expand Up @@ -207,6 +228,62 @@
"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",
],
}

Expand Down
2 changes: 1 addition & 1 deletion src/sunsynk/definitions/three_phase_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions src/tests/ha_addon_sunsynk_multi/test_sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
146 changes: 145 additions & 1 deletion www/docs/reference/definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading