Skip to content

Fix unique id of solar to match v1.0.10 format v2 #89

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

Merged
merged 1 commit into from
Jun 24, 2025
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
4 changes: 2 additions & 2 deletions custom_components/span_panel/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,8 +844,8 @@ async def async_setup_entry(
# Create device info for solar sensors
device_info = panel_to_device_info(span_panel)

# Configure sensor manager for device integration with v1.0.10-compatible prefix
# Generate prefix to match v1.0.10 unique ID format for seamless upgrades
# Configure sensor manager for v1.0.10 compatibility
# Use same prefix format as v1.0.10: span_{serial}_synthetic_{circuits}
circuit_spec = "_".join(
str(num) for num in [inverter_leg1, inverter_leg2] if num > 0
)
Expand Down
24 changes: 10 additions & 14 deletions custom_components/span_panel/solar_synthetic_sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,28 +641,23 @@ def _build_solar_sensors(
return solar_sensors

def _generate_circuit_based_key(self, leg1: int, leg2: int, suffix: str) -> str:
"""Generate circuit-based YAML key that matches v1.0.10 unique_id format.
"""Generate YAML key that matches v1.0.10 unique_id format.

This creates keys like 'span_panel_solar_inverter_2_14_instant_power' to match
the unique_id format used in v1.0.10, preserving historical data during upgrades.
In v1.0.10, unique_ids were: span_{serial}_synthetic_{circuits}_{key}
With prefix: span_{serial}_synthetic_{circuits}
The YAML key should be: solar_inverter_{suffix}

Args:
leg1: First solar inverter leg circuit number
leg2: Second solar inverter leg circuit number
suffix: Sensor type suffix (instant_power, energy_produced, energy_consumed)

Returns:
Circuit-based key for YAML configuration
YAML key for configuration

"""
# Generate key based on circuit numbers, matching v1.0.10 format
# Format: span_panel_solar_inverter_{leg1}_{leg2}_{suffix}
if leg2 > 0:
# Two legs configured
return f"span_panel_solar_inverter_{leg1}_{leg2}_{suffix}"
else:
# Single leg configured
return f"span_panel_solar_inverter_{leg1}_{suffix}"
# Generate key matching v1.0.10 format: solar_inverter_{suffix}
return f"solar_inverter_{suffix}"

def _merge_solar_into_config(
self, existing_config: dict[str, Any], solar_sensors: dict[str, Any]
Expand All @@ -673,11 +668,12 @@ def _merge_solar_into_config(
existing_config["sensors"] = {}

# Remove any existing solar sensors first (to handle circuit number changes)
# Solar sensors have keys that start with "span_panel_solar_inverter_"
# Solar sensors have keys that start with "solar_inverter_" (v1.0.10 format)
# or "span_panel_solar_inverter_" (old circuit-based format)
solar_keys_to_remove = [
key
for key in existing_config["sensors"]
if key.startswith("span_panel_solar_inverter_")
if key.startswith("solar_inverter_") or key.startswith("span_panel_solar_inverter_")
]

for key in solar_keys_to_remove:
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions tests/fixtures/span-ha-synthetic-mixed-references.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: "1.0"
sensors:
# Solar inverter sensors with SPAN circuit variables (should update when circuit names change)
span_panel_solar_inverter_30_32_instant_power:
solar_inverter_instant_power:
name: "Solar Inverter Instant Power"
formula: "leg1_power + leg2_power"
variables:
Expand All @@ -12,7 +12,7 @@ sensors:
state_class: "measurement"
entity_id: "sensor.span_panel_solar_inverter_instant_power"

span_panel_solar_inverter_30_32_energy_produced:
solar_inverter_energy_produced:
name: "Solar Inverter Energy Produced"
formula: "leg1_produced + leg2_produced"
variables:
Expand Down
6 changes: 3 additions & 3 deletions tests/test_coordinator_migration_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ async def test_yaml_config_generates_correct_entity_names(

# Check for expected solar sensors (circuit-based keys for v1.0.10 compatibility)
expected_sensors = {
"span_panel_solar_inverter_30_32_instant_power": "Solar Inverter Instant Power",
"span_panel_solar_inverter_30_32_energy_produced": "Solar Inverter Energy Produced",
"span_panel_solar_inverter_30_32_energy_consumed": "Solar Inverter Energy Consumed",
"solar_inverter_instant_power": "Solar Inverter Instant Power",
"solar_inverter_energy_produced": "Solar Inverter Energy Produced",
"solar_inverter_energy_consumed": "Solar Inverter Energy Consumed",
}

for sensor_key, expected_name in expected_sensors.items():
Expand Down
6 changes: 3 additions & 3 deletions tests/test_entity_id_in_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ async def test_entity_id_fields_present_in_yaml(
# With USE_CIRCUIT_NUMBERS: False, we get name-based IDs
# But YAML keys are circuit-based for v1.0.10 compatibility
expected_sensors = {
"span_panel_solar_inverter_15_16_instant_power": "sensor.span_panel_solar_inverter_instant_power",
"span_panel_solar_inverter_15_16_energy_produced": "sensor.span_panel_solar_inverter_energy_produced",
"span_panel_solar_inverter_15_16_energy_consumed": "sensor.span_panel_solar_inverter_energy_consumed",
"solar_inverter_instant_power": "sensor.span_panel_solar_inverter_instant_power",
"solar_inverter_energy_produced": "sensor.span_panel_solar_inverter_energy_produced",
"solar_inverter_energy_consumed": "sensor.span_panel_solar_inverter_energy_consumed",
}

for sensor_key, expected_entity_id in expected_sensors.items():
Expand Down
12 changes: 3 additions & 9 deletions tests/test_entity_id_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,12 @@ async def test_entity_id_migration_legacy_to_modern(
updated_yaml = yaml.safe_load(f)

# Verify entity_id migration
solar_power_sensor = updated_yaml["sensors"][
"span_panel_solar_inverter_30_32_instant_power"
]
solar_power_sensor = updated_yaml["sensors"]["solar_inverter_instant_power"]
assert (
solar_power_sensor["entity_id"] == "sensor.span_panel_solar_inverter_instant_power"
)

solar_energy_sensor = updated_yaml["sensors"][
"span_panel_solar_inverter_30_32_energy_produced"
]
solar_energy_sensor = updated_yaml["sensors"]["solar_inverter_energy_produced"]
assert (
solar_energy_sensor["entity_id"]
== "sensor.span_panel_solar_inverter_energy_produced"
Expand Down Expand Up @@ -187,9 +183,7 @@ async def test_migration_skipped_when_device_prefix_disabled(

# Verify legacy entity_id is preserved (no migration)
solar_power_sensor = updated_yaml["sensors"]["solar_inverter_instant_power"]
assert (
solar_power_sensor["entity_id"] == "sensor.span_panel_solar_inverter_instant_power"
)
assert solar_power_sensor["entity_id"] == "sensor.solar_inverter_instant_power"

async def test_migration_handles_missing_yaml_gracefully(
self,
Expand Down
14 changes: 7 additions & 7 deletions tests/test_solar_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ async def test_generate_solar_config_dual_legs(

# With stable naming, the keys should be based on friendly names, not circuit numbers
# Check solar inverter instant power sensor
power_sensor = config["sensors"]["span_panel_solar_inverter_15_16_instant_power"]
power_sensor = config["sensors"]["solar_inverter_instant_power"]
assert power_sensor["name"] == "Solar Inverter Instant Power"
assert power_sensor["formula"] == "leg1_power + leg2_power"
# The entity IDs should use the stable unmapped naming logic
Expand All @@ -203,7 +203,7 @@ async def test_generate_solar_config_dual_legs(
assert power_sensor["state_class"] == "measurement"

# Check energy produced sensor
produced_sensor = config["sensors"]["span_panel_solar_inverter_15_16_energy_produced"]
produced_sensor = config["sensors"]["solar_inverter_energy_produced"]
assert produced_sensor["name"] == "Solar Inverter Energy Produced"
assert produced_sensor["formula"] == "leg1_produced + leg2_produced"
# The entity IDs should use the stable unmapped naming logic
Expand All @@ -220,7 +220,7 @@ async def test_generate_solar_config_dual_legs(
assert produced_sensor["state_class"] == "total_increasing"

# Check energy consumed sensor
consumed_sensor = config["sensors"]["span_panel_solar_inverter_15_16_energy_consumed"]
consumed_sensor = config["sensors"]["solar_inverter_energy_consumed"]
assert consumed_sensor["name"] == "Solar Inverter Energy Consumed"
assert consumed_sensor["formula"] == "leg1_consumed + leg2_consumed"
# The entity IDs should use the stable unmapped naming logic
Expand Down Expand Up @@ -254,7 +254,7 @@ async def test_generate_solar_config_single_leg(
config = yaml.safe_load(f)

# Check single-leg formulas - should still be solar inverter sensors
power_sensor = config["sensors"]["span_panel_solar_inverter_15_instant_power"]
power_sensor = config["sensors"]["solar_inverter_instant_power"]
assert power_sensor["formula"] == "leg1_power"
assert "leg1_power" in power_sensor["variables"]
assert "leg2_power" not in power_sensor["variables"]
Expand Down Expand Up @@ -478,9 +478,9 @@ async def test_yaml_compliance_with_ha_synthetic_sensors(

# Verify sensor keys match expected naming (stable solar inverter naming)
expected_sensors = [
"span_panel_solar_inverter_15_16_instant_power",
"span_panel_solar_inverter_15_16_energy_produced",
"span_panel_solar_inverter_15_16_energy_consumed",
"solar_inverter_instant_power",
"solar_inverter_energy_produced",
"solar_inverter_energy_consumed",
]
for sensor_key in expected_sensors:
assert sensor_key in config["sensors"]
Expand Down
24 changes: 9 additions & 15 deletions tests/test_yaml_regeneration.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ async def test_yaml_variables_update_when_circuits_change(
initial_yaml = yaml.safe_load(f)

# Verify initial variables point to the original circuit names
power_sensor = initial_yaml["sensors"]["span_panel_solar_inverter_30_32_instant_power"]
power_sensor = initial_yaml["sensors"]["solar_inverter_instant_power"]
assert power_sensor["variables"]["leg1_power"] == "sensor.span_panel_main_kitchen_power"
assert power_sensor["variables"]["leg2_power"] == "sensor.span_panel_main_garage_power"

Expand Down Expand Up @@ -132,15 +132,13 @@ async def test_yaml_variables_update_when_circuits_change(
updated_yaml = yaml.safe_load(f)

# Verify variables now point to the new friendly names
updated_power_sensor = updated_yaml["sensors"][
"span_panel_solar_inverter_30_32_instant_power"
]
updated_power_sensor = updated_yaml["sensors"]["solar_inverter_instant_power"]
# The entity IDs should now be based on the friendly names, not the original names
assert "solar_east" in updated_power_sensor["variables"]["leg1_power"]
assert "solar_west" in updated_power_sensor["variables"]["leg2_power"]

# But the sensor key itself should remain stable
assert "span_panel_solar_inverter_30_32_instant_power" in updated_yaml["sensors"]
assert "solar_inverter_instant_power" in updated_yaml["sensors"]

async def test_yaml_variables_update_for_single_vs_dual_leg(
self,
Expand All @@ -160,7 +158,7 @@ async def test_yaml_variables_update_for_single_vs_dual_leg(
dual_leg_yaml = yaml.safe_load(f)

# Verify dual leg formula and variables
power_sensor = dual_leg_yaml["sensors"]["span_panel_solar_inverter_30_32_instant_power"]
power_sensor = dual_leg_yaml["sensors"]["solar_inverter_instant_power"]
assert power_sensor["formula"] == "leg1_power + leg2_power"
assert "leg1_power" in power_sensor["variables"]
assert "leg2_power" in power_sensor["variables"]
Expand All @@ -172,9 +170,7 @@ async def test_yaml_variables_update_for_single_vs_dual_leg(
single_leg_yaml = yaml.safe_load(f)

# Verify single leg formula and variables
single_power_sensor = single_leg_yaml["sensors"][
"span_panel_solar_inverter_30_instant_power"
]
single_power_sensor = single_leg_yaml["sensors"]["solar_inverter_instant_power"]
assert single_power_sensor["formula"] == "leg1_power"
assert "leg1_power" in single_power_sensor["variables"]
assert "leg2_power" not in single_power_sensor["variables"]
Expand Down Expand Up @@ -241,17 +237,15 @@ async def test_yaml_variable_updates_with_mixed_references(
updated_yaml = yaml.safe_load(f)

# Verify SPAN circuit variables were updated
solar_power = updated_yaml["sensors"]["span_panel_solar_inverter_30_32_instant_power"]
solar_power = updated_yaml["sensors"]["solar_inverter_instant_power"]
assert (
solar_power["variables"]["leg1_power"] == "sensor.span_panel_kitchen_updated_power"
)
assert (
solar_power["variables"]["leg2_power"] == "sensor.span_panel_garage_updated_power"
)

solar_energy = updated_yaml["sensors"][
"span_panel_solar_inverter_30_32_energy_produced"
]
solar_energy = updated_yaml["sensors"]["solar_inverter_energy_produced"]
assert (
solar_energy["variables"]["leg1_produced"]
== "sensor.span_panel_kitchen_updated_energy_produced"
Expand Down Expand Up @@ -293,8 +287,8 @@ async def test_yaml_variable_updates_with_mixed_references(

# Verify the sensor keys themselves remain stable (solar inverter naming)
expected_sensor_keys = {
"span_panel_solar_inverter_30_32_instant_power",
"span_panel_solar_inverter_30_32_energy_produced",
"solar_inverter_instant_power",
"solar_inverter_energy_produced",
"span_panel_total_house_consumption",
"external_weather_calculation",
"span_panel_unmapped_total",
Expand Down
26 changes: 9 additions & 17 deletions tests/test_yaml_regeneration_clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ async def test_yaml_variables_update_when_circuits_renamed(
initial_yaml = yaml.safe_load(f)

# Verify initial variables use unmapped tab names
power_sensor = initial_yaml["sensors"]["span_panel_solar_inverter_30_32_instant_power"]
power_sensor = initial_yaml["sensors"]["solar_inverter_instant_power"]
assert "unmapped_tab_30" in power_sensor["variables"]["leg1_power"]
assert "unmapped_tab_32" in power_sensor["variables"]["leg2_power"]

Expand All @@ -96,14 +96,12 @@ async def test_yaml_variables_update_when_circuits_renamed(
updated_yaml = yaml.safe_load(f)

# Verify variables still use stable unmapped tab names (not friendly names)
updated_power_sensor = updated_yaml["sensors"][
"span_panel_solar_inverter_30_32_instant_power"
]
updated_power_sensor = updated_yaml["sensors"]["solar_inverter_instant_power"]
assert "unmapped_tab_30" in updated_power_sensor["variables"]["leg1_power"]
assert "unmapped_tab_32" in updated_power_sensor["variables"]["leg2_power"]

# Verify sensor keys remain stable
assert "span_panel_solar_inverter_30_32_instant_power" in updated_yaml["sensors"]
assert "solar_inverter_instant_power" in updated_yaml["sensors"]
assert updated_power_sensor["entity_id"] == power_sensor["entity_id"]

async def test_yaml_handles_circuit_addition_and_removal(
Expand All @@ -125,7 +123,7 @@ async def test_yaml_handles_circuit_addition_and_removal(
single_leg_yaml = yaml.safe_load(f)

# Verify single leg formula
power_sensor = single_leg_yaml["sensors"]["span_panel_solar_inverter_30_instant_power"]
power_sensor = single_leg_yaml["sensors"]["solar_inverter_instant_power"]
assert power_sensor["formula"] == "leg1_power"
assert "leg1_power" in power_sensor["variables"]
assert "leg2_power" not in power_sensor["variables"]
Expand All @@ -137,9 +135,7 @@ async def test_yaml_handles_circuit_addition_and_removal(
dual_leg_yaml = yaml.safe_load(f)

# Verify dual leg formula
dual_power_sensor = dual_leg_yaml["sensors"][
"span_panel_solar_inverter_30_32_instant_power"
]
dual_power_sensor = dual_leg_yaml["sensors"]["solar_inverter_instant_power"]
assert dual_power_sensor["formula"] == "leg1_power + leg2_power"
assert "leg1_power" in dual_power_sensor["variables"]
assert "leg2_power" in dual_power_sensor["variables"]
Expand Down Expand Up @@ -167,7 +163,7 @@ async def test_yaml_variables_survive_circuit_id_changes(
initial_yaml = yaml.safe_load(f)

# Verify initial circuit variables
power_sensor = initial_yaml["sensors"]["span_panel_solar_inverter_30_32_instant_power"]
power_sensor = initial_yaml["sensors"]["solar_inverter_instant_power"]
initial_leg1 = power_sensor["variables"]["leg1_power"]
initial_leg2 = power_sensor["variables"]["leg2_power"]

Expand All @@ -191,9 +187,7 @@ async def test_yaml_variables_survive_circuit_id_changes(
updated_yaml = yaml.safe_load(f)

# Verify variables now point to new circuits
updated_power_sensor = updated_yaml["sensors"][
"span_panel_solar_inverter_28_29_instant_power"
]
updated_power_sensor = updated_yaml["sensors"]["solar_inverter_instant_power"]
updated_leg1 = updated_power_sensor["variables"]["leg1_power"]
updated_leg2 = updated_power_sensor["variables"]["leg2_power"]

Expand Down Expand Up @@ -229,7 +223,7 @@ async def test_unmapped_circuits_maintain_stable_entity_ids(
initial_yaml = yaml.safe_load(f)

# Get initial variables
power_sensor = initial_yaml["sensors"]["span_panel_solar_inverter_30_32_instant_power"]
power_sensor = initial_yaml["sensors"]["solar_inverter_instant_power"]
initial_leg1 = power_sensor["variables"]["leg1_power"]
initial_leg2 = power_sensor["variables"]["leg2_power"]

Expand All @@ -245,9 +239,7 @@ async def test_unmapped_circuits_maintain_stable_entity_ids(
updated_yaml = yaml.safe_load(f)

# Verify variables remain the same for unmapped circuits
updated_power_sensor = updated_yaml["sensors"][
"span_panel_solar_inverter_30_32_instant_power"
]
updated_power_sensor = updated_yaml["sensors"]["solar_inverter_instant_power"]
updated_leg1 = updated_power_sensor["variables"]["leg1_power"]
updated_leg2 = updated_power_sensor["variables"]["leg2_power"]

Expand Down