From dc6b2dc71ad5fbedde7f73d30dc4ce945c0cf442 Mon Sep 17 00:00:00 2001 From: Jens Neuber Date: Mon, 10 Aug 2026 11:59:29 +0200 Subject: [PATCH] Group optimizer sensors under site device --- README.md | 34 ++++++++------- .../solaredge_optimizers/__init__.py | 43 ++++++++++++++++++- .../solaredge_optimizers/config_flow.py | 3 +- .../solaredge_optimizers/const.py | 1 + .../solaredge_optimizers/manifest.json | 2 +- .../solaredge_optimizers/sensor.py | 16 ++++--- .../solaredge_optimizers/strings.json | 5 +-- .../solaredge_optimizers/translations/de.json | 5 +-- .../solaredge_optimizers/translations/en.json | 5 +-- pyproject.toml | 2 +- tests/test_sensor.py | 28 ++++++++---- uv.lock | 2 +- 12 files changed, 102 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 5d539a0..4572c42 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,19 @@ Diese Custom Integration liest den lokalen Cache der [SolarEdge Optimizer Data App](https://git.jensneuber.de/jens/solaredge-optimizers) -und stellt jeden Leistungsoptimierer als eigenes Home-Assistant-Gerät bereit. +und stellt die Messwerte jedes Leistungsoptimierers als Sensoren bereit. + +Alle Optimierer werden unter einem gemeinsamen Home-Assistant-Gerät pro +SolarEdge-Site zusammengefasst. Pro Optimierer werden zwei Sensoren angelegt: - **Tagesenergie** in Wh (`total_increasing`) - **Aktuelle Leistung** in W (`measurement`) -Die Seriennummer bleibt die stabile Gerätekennung. Optimierer-ID, String-ID, -Wechselrichter-ID und Zeitpunkt der letzten Messung stehen als Attribute zur -Verfügung. +Die Seriennummer bleibt Bestandteil der stabilen Entity-ID. Optimierer-ID, +String-ID, Wechselrichter-ID und Zeitpunkt der letzten Messung stehen als +Attribute zur Verfügung. ## Voraussetzungen @@ -22,18 +25,20 @@ Verfügung. Die SolarEdge-Zugangsdaten bleiben ausschließlich in der App-Konfiguration. Die Integration spricht nur mit der lokalen App-API. -## Installation mit HACS +## Installation -1. In HACS **Integrationen** öffnen. -2. Über das Menü **Benutzerdefinierte Repositories** öffnen. -3. `https://git.jensneuber.de/jens/ha-solaredge-optimizers` als Typ - **Integration** hinzufügen. -4. **SolarEdge Optimizers** installieren. -5. Home Assistant neu starten. +HACS unterstützt keine Gitea-Repositories. Lade deshalb das Archiv der +gewünschten Version herunter und entpacke es: -Alternativ den Ordner `custom_components/solaredge_optimizers` nach -`/config/custom_components/solaredge_optimizers` kopieren und Home Assistant -neu starten. +```text +https://git.jensneuber.de/jens/ha-solaredge-optimizers/archive/v0.2.0.zip +``` + +Kopiere anschließend den enthaltenen Ordner +`custom_components/solaredge_optimizers` nach +`/config/custom_components/solaredge_optimizers` und starte Home Assistant neu. +Eine Installation über HACS ist erst mit einem öffentlichen GitHub-Mirror +möglich. ## Einrichtung @@ -59,4 +64,3 @@ uv sync uv run ruff check . uv run pytest ``` - diff --git a/custom_components/solaredge_optimizers/__init__.py b/custom_components/solaredge_optimizers/__init__.py index 002e536..6a74a03 100644 --- a/custom_components/solaredge_optimizers/__init__.py +++ b/custom_components/solaredge_optimizers/__init__.py @@ -7,16 +7,57 @@ from datetime import timedelta from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from .api import SolarEdgeOptimizerApiClient -from .const import CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL +from .const import ( + CONF_SCAN_INTERVAL, + CONFIG_ENTRY_VERSION, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + LOGGER, +) from .coordinator import SolarEdgeOptimizerCoordinator PLATFORMS = [Platform.SENSOR] type SolarEdgeOptimizerConfigEntry = ConfigEntry[SolarEdgeOptimizerCoordinator] +async def async_migrate_entry( + hass: HomeAssistant, entry: SolarEdgeOptimizerConfigEntry +) -> bool: + """Migrate optimizer devices to one site device.""" + if entry.version > CONFIG_ENTRY_VERSION: + LOGGER.error( + "Cannot migrate config entry from version %s to %s", + entry.version, + CONFIG_ENTRY_VERSION, + ) + return False + + if entry.version < 2: + site_id = entry.unique_id + if site_id: + device_registry = dr.async_get(hass) + legacy_prefix = f"{site_id}_" + for device in dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ): + if any( + domain == DOMAIN and identifier.startswith(legacy_prefix) + for domain, identifier in device.identifiers + ): + device_registry.async_remove_device(device.id) + + hass.config_entries.async_update_entry( + entry, + version=CONFIG_ENTRY_VERSION, + ) + + return True + + async def async_setup_entry( hass: HomeAssistant, entry: SolarEdgeOptimizerConfigEntry ) -> bool: diff --git a/custom_components/solaredge_optimizers/config_flow.py b/custom_components/solaredge_optimizers/config_flow.py index 7809708..39d50ff 100644 --- a/custom_components/solaredge_optimizers/config_flow.py +++ b/custom_components/solaredge_optimizers/config_flow.py @@ -20,6 +20,7 @@ from .api import ( ) from .const import ( CONF_SCAN_INTERVAL, + CONFIG_ENTRY_VERSION, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN, @@ -61,7 +62,7 @@ def _schema(defaults: dict[str, Any]) -> vol.Schema: class SolarEdgeOptimizersConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for SolarEdge Optimizers.""" - VERSION = 1 + VERSION = CONFIG_ENTRY_VERSION def __init__(self) -> None: """Initialize the flow.""" diff --git a/custom_components/solaredge_optimizers/const.py b/custom_components/solaredge_optimizers/const.py index 0d62e85..9890f19 100644 --- a/custom_components/solaredge_optimizers/const.py +++ b/custom_components/solaredge_optimizers/const.py @@ -5,6 +5,7 @@ from datetime import timedelta DOMAIN = "solaredge_optimizers" LOGGER = logging.getLogger(__package__) +CONFIG_ENTRY_VERSION = 2 CONF_SCAN_INTERVAL = "scan_interval" DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) diff --git a/custom_components/solaredge_optimizers/manifest.json b/custom_components/solaredge_optimizers/manifest.json index ae2e73b..1398403 100644 --- a/custom_components/solaredge_optimizers/manifest.json +++ b/custom_components/solaredge_optimizers/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "issue_tracker": "https://git.jensneuber.de/jens/ha-solaredge-optimizers/issues", "requirements": [], - "version": "0.1.0" + "version": "0.2.0" } diff --git a/custom_components/solaredge_optimizers/sensor.py b/custom_components/solaredge_optimizers/sensor.py index 503fe25..07cd6f6 100644 --- a/custom_components/solaredge_optimizers/sensor.py +++ b/custom_components/solaredge_optimizers/sensor.py @@ -102,6 +102,10 @@ class SolarEdgeOptimizerSensor( self.entity_description = description self._serial = serial self._attr_unique_id = f"{coordinator.data.site_id}_{serial}_{description.key}" + optimizer = coordinator.data.optimizers_by_serial[serial] + self._attr_translation_placeholders = { + "optimizer_id": optimizer.optimizer_id or serial + } @property def _optimizer(self) -> OptimizerData | None: @@ -121,15 +125,13 @@ class SolarEdgeOptimizerSensor( @property def device_info(self) -> DeviceInfo: - """Return optimizer device information.""" - optimizer = self._optimizer - optimizer_id = optimizer.optimizer_id if optimizer else None + """Return the shared SolarEdge site device information.""" + site_id = self.coordinator.data.site_id return DeviceInfo( - identifiers={(DOMAIN, f"{self.coordinator.data.site_id}_{self._serial}")}, + identifiers={(DOMAIN, site_id)}, manufacturer="SolarEdge", - model="Power Optimizer", - name=f"Optimizer {optimizer_id or self._serial}", - serial_number=self._serial, + model="Optimizer Monitoring Site", + name=f"SolarEdge Site {site_id}", ) @property diff --git a/custom_components/solaredge_optimizers/strings.json b/custom_components/solaredge_optimizers/strings.json index e7aee2b..3aea159 100644 --- a/custom_components/solaredge_optimizers/strings.json +++ b/custom_components/solaredge_optimizers/strings.json @@ -46,12 +46,11 @@ "entity": { "sensor": { "daily_energy": { - "name": "Daily energy" + "name": "{optimizer_id} daily energy" }, "current_power": { - "name": "Current power" + "name": "{optimizer_id} current power" } } } } - diff --git a/custom_components/solaredge_optimizers/translations/de.json b/custom_components/solaredge_optimizers/translations/de.json index b4c4f3f..ae7dfe3 100644 --- a/custom_components/solaredge_optimizers/translations/de.json +++ b/custom_components/solaredge_optimizers/translations/de.json @@ -46,12 +46,11 @@ "entity": { "sensor": { "daily_energy": { - "name": "Tagesenergie" + "name": "{optimizer_id} Tagesenergie" }, "current_power": { - "name": "Aktuelle Leistung" + "name": "{optimizer_id} aktuelle Leistung" } } } } - diff --git a/custom_components/solaredge_optimizers/translations/en.json b/custom_components/solaredge_optimizers/translations/en.json index e7aee2b..3aea159 100644 --- a/custom_components/solaredge_optimizers/translations/en.json +++ b/custom_components/solaredge_optimizers/translations/en.json @@ -46,12 +46,11 @@ "entity": { "sensor": { "daily_energy": { - "name": "Daily energy" + "name": "{optimizer_id} daily energy" }, "current_power": { - "name": "Current power" + "name": "{optimizer_id} current power" } } } } - diff --git a/pyproject.toml b/pyproject.toml index 0c88bce..6bf2e21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ha-solaredge-optimizers" -version = "0.1.0" +version = "0.2.0" description = "Home Assistant custom integration for SolarEdge optimizer data" readme = "README.md" requires-python = ">=3.14.2" diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 3dd3455..0188953 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -25,6 +25,15 @@ async def test_optimizer_sensors(hass: HomeAssistant) -> None: data={CONF_URL: "http://app:8099", CONF_SCAN_INTERVAL: 600}, ) entry.add_to_hass(hass) + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "4886699_14F28854-E2")}, + manufacturer="SolarEdge", + model="Power Optimizer", + name="Optimizer 1.1.1", + serial_number="14F28854-E2", + ) with patch( "custom_components.solaredge_optimizers.api." @@ -44,13 +53,16 @@ async def test_optimizer_sensors(hass: HomeAssistant) -> None: power_entity = next( entity for entity in entities if entity.unique_id.endswith("_current_power") ) - assert hass.states.get(energy_entity.entity_id).state == "1234.5" - assert hass.states.get(power_entity.entity_id).state == "160.7" + energy_state = hass.states.get(energy_entity.entity_id) + power_state = hass.states.get(power_entity.entity_id) + assert energy_state.state == "1234.5" + assert power_state.state == "160.7" + assert "1.1.1" in energy_state.name + assert "1.1.1" in power_state.name - device_registry = dr.async_get(hass) devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) - assert len(devices) == 2 - assert {device.serial_number for device in devices} == { - "14F28854-E2", - "14F28855-E2", - } + assert len(devices) == 1 + assert devices[0].identifiers == {(DOMAIN, "4886699")} + assert devices[0].name == "SolarEdge Site 4886699" + assert all(entity.device_id == devices[0].id for entity in entities) + assert entry.version == 2 diff --git a/uv.lock b/uv.lock index 6bad467..cf467f4 100644 --- a/uv.lock +++ b/uv.lock @@ -943,7 +943,7 @@ wheels = [ [[package]] name = "ha-solaredge-optimizers" -version = "0.1.0" +version = "0.2.0" source = { virtual = "." } [package.dev-dependencies]