diff --git a/README.md b/README.md index 342169d..a33262f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ista EcoTrend Version 2 +# ista EcoTrend Version 3 [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://img.shields.io/badge/My-HACS:%20REPOSITORY-000000.svg?&style=for-the-badge&logo=home-assistant&logoColor=white&color=049cdb)](https://my.home-assistant.io/redirect/hacs_repository/?owner=Ludy87&repository=ecotrend-ista&category=integration) [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge&logo=home-assistant&logoColor=white)](https://github.com/hacs/integration) @@ -14,7 +14,7 @@ [![Buy me a coffee](https://img.shields.io/static/v1.svg?label=Buy%20me%20a%20coffee&message=donate&style=for-the-badge&color=black&logo=buy%20me%20a%20coffee&logoColor=white&labelColor=orange)](https://www.buymeacoffee.com/ludy87) --- -![ista EcoTrend V2](https://github.com/Ludy87/ecotrend-ista/blob/main/image/logo_new@2x.png?raw=true) +![ista EcoTrend V3](https://github.com/Ludy87/ecotrend-ista/blob/main/image/logo_new@2x.png?raw=true) ## Installation diff --git a/custom_components/ecotrend_ista/__init__.py b/custom_components/ecotrend_ista/__init__.py index 3228c93..83bc8b3 100644 --- a/custom_components/ecotrend_ista/__init__.py +++ b/custom_components/ecotrend_ista/__init__.py @@ -1,4 +1,4 @@ -"""ista EcoTrend Version 2.""" +"""ista EcoTrend Version 3.""" from __future__ import annotations import logging @@ -7,7 +7,8 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.typing import ConfigType from .const import DATA_HASS_CONFIG, DOMAIN @@ -26,8 +27,8 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: - """Set up the ista EcoTrend Version 2 component.""" - _LOGGER.debug("Set up the ista EcoTrend Version 2 component") + """Set up the ista EcoTrend Version 3 component.""" + _LOGGER.debug("Set up the ista EcoTrend Version 3 component") hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][DATA_HASS_CONFIG] = hass_config if DOMAIN in hass_config: @@ -49,6 +50,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Configure based on config entry %s", entry.entry_id) coordinator = IstaDataUpdateCoordinator(hass, entry) await coordinator.init() + for uuid in coordinator.controller.getUUIDs(): + await _async_migrate_entries( + hass, + entry, + uuid, + coordinator.controller.getSupportCode(), + ) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) @@ -73,5 +81,37 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def options_update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Handle options update.""" - _LOGGER.debug("Configuration options updated, reloading ista EcoTrend 2 integration") + _LOGGER.debug("Configuration options updated, reloading ista EcoTrend 3 integration") await hass.config_entries.async_reload(config_entry.entry_id) + + +async def _async_migrate_entries( + hass: HomeAssistant, + config_entry: ConfigEntry, + new_uid: str, + support_code: str, +) -> bool: + """Migrate old entry.""" + entity_registry = er.async_get(hass) + + @callback + def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None: + if support_code in str(entry.unique_id): + # heating_custom_{support_code} old + # heating_custom_{new_uid} new + new_unique_id = str(entry.unique_id).replace(support_code, new_uid).replace("-", "_").replace(" ", "_").lower() + _LOGGER.debug( + "change unique_id - entity: '%s' unique_id from '%s' to '%s'", + entry.entity_id, + entry.unique_id, + new_unique_id, + ) + if existing_entity_id := entity_registry.async_get_entity_id(entry.domain, entry.platform, new_unique_id): + _LOGGER.debug("Cannot change unique_id to '%s', already exists for '%s'", new_unique_id, existing_entity_id) + return None + return {"new_unique_id": new_unique_id} + return None + + await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + + return True diff --git a/custom_components/ecotrend_ista/config_flow.py b/custom_components/ecotrend_ista/config_flow.py index 2af0b25..c0e2ca1 100644 --- a/custom_components/ecotrend_ista/config_flow.py +++ b/custom_components/ecotrend_ista/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for ista EcoTrend Version 2.""" +"""Config flow for ista EcoTrend Version 3.""" from __future__ import annotations import copy @@ -81,7 +81,7 @@ class NotSupportedURL(Exception): class IstaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for ista EcoTrend Version 2.""" + """Handle a config flow for ista EcoTrend Version 3.""" VERSION = 1 @@ -145,7 +145,7 @@ async def async_step_german(self, user_input: dict[str, Any] | None = None) -> F ) async def async_step_import(self, import_data: dict[str, Any]): - """Import ista EcoTrend Version 2 config from configuration.yaml.""" + """Import ista EcoTrend Version 3 config from configuration.yaml.""" _import_data = copy.deepcopy(import_data) _import_data[CONF_PASSWORD] = "*****" diff --git a/custom_components/ecotrend_ista/const.py b/custom_components/ecotrend_ista/const.py index 6dc3197..088f9f4 100644 --- a/custom_components/ecotrend_ista/const.py +++ b/custom_components/ecotrend_ista/const.py @@ -1,4 +1,4 @@ -"""Const for ista EcoTrend Version 2.""" +"""Const for ista EcoTrend Version 3.""" from __future__ import annotations from typing import Final diff --git a/custom_components/ecotrend_ista/const_schema.py b/custom_components/ecotrend_ista/const_schema.py index aec0e9e..b05be68 100644 --- a/custom_components/ecotrend_ista/const_schema.py +++ b/custom_components/ecotrend_ista/const_schema.py @@ -1,4 +1,4 @@ -"""Const schema for ista EcoTrend Version 2.""" +"""Const schema for ista EcoTrend Version 3.""" from __future__ import annotations import voluptuous as vol diff --git a/custom_components/ecotrend_ista/coordinator.py b/custom_components/ecotrend_ista/coordinator.py index 78ead83..5d2238a 100644 --- a/custom_components/ecotrend_ista/coordinator.py +++ b/custom_components/ecotrend_ista/coordinator.py @@ -1,4 +1,4 @@ -"""Coordinator for ista EcoTrend Version 2.""" +"""Coordinator for ista EcoTrend Version 3.""" from __future__ import annotations import datetime @@ -45,12 +45,12 @@ def make_file() -> None: class IstaDataUpdateCoordinator(DataUpdateCoordinator): - """Coordinator for ista EcoTrend Version 2.""" + """Coordinator for ista EcoTrend Version 3.""" controller: PyEcotrendIsta def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: - """Initialize ista EcoTrend Version 2 data updater.""" + """Initialize ista EcoTrend Version 3 data updater.""" self._entry = entry super().__init__( hass=hass, @@ -79,26 +79,32 @@ async def init(self) -> None: await self.hass.async_add_executor_job(self.controller.login) async def _async_update_data(self): - """Update the data from ista EcoTrend Version 2.""" + """Update the data from ista EcoTrend Version 3.""" try: + if self.data is None: + self.data = {} await self.init() - _consum_raw: dict[str, Any] = await self.hass.async_add_executor_job( - self.controller.consum_raw, - [ - datetime.datetime.now().year, - datetime.datetime.now().year - 1, - ], - ) - if not isinstance(_consum_raw, dict): - return self.data - consum_raw: CustomRaw = CustomRaw.from_dict(_consum_raw) - - await create_directory_file( - self.hass, - consum_raw, - self.controller.getSupportCode(), - ) - self.data = consum_raw + for uuid in self.controller.getUUIDs(): + _consum_raw: dict[str, Any] = await self.hass.async_add_executor_job( + self.controller.consum_raw, + [ + datetime.datetime.now().year, + datetime.datetime.now().year - 1, + ], + None, + True, + uuid, + ) + if not isinstance(_consum_raw, dict): + return self.data[uuid] + consum_raw: CustomRaw = CustomRaw.from_dict(_consum_raw) + + await create_directory_file( + self.hass, + consum_raw, + self.controller.getSupportCode(), + ) + self.data[uuid] = consum_raw self.async_set_updated_data(self.data) return self.data except requests.Timeout: diff --git a/custom_components/ecotrend_ista/manifest.json b/custom_components/ecotrend_ista/manifest.json index 1f342e2..306dd6c 100644 --- a/custom_components/ecotrend_ista/manifest.json +++ b/custom_components/ecotrend_ista/manifest.json @@ -11,9 +11,9 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/Ludy87/ecotrend-ista/issues", "requirements": [ - "pyecotrend_ista==2.3.0", + "pyecotrend_ista==3.0.0", "pyotp==2.8.0", "marshmallow-enum==1.5.1" ], - "version": "v2.3.0" + "version": "v3.0.0" } diff --git a/custom_components/ecotrend_ista/sensor.py b/custom_components/ecotrend_ista/sensor.py index ba433e2..f8c02e5 100644 --- a/custom_components/ecotrend_ista/sensor.py +++ b/custom_components/ecotrend_ista/sensor.py @@ -33,24 +33,25 @@ _LOGGER = logging.getLogger(__name__) -class EcotrendBaseEntityV2(CoordinatorEntity[IstaDataUpdateCoordinator], RestoreSensor): - """Base entity class for ista EcoTrend Version 2.""" +class EcotrendBaseEntityV3(CoordinatorEntity[IstaDataUpdateCoordinator], RestoreSensor): + """Base entity class for ista EcoTrend Version 3.""" _attr_force_update = False - def __init__(self, coordinator: IstaDataUpdateCoordinator, controller: PyEcotrendIsta) -> None: - """Initialize the ista EcoTrend Version 2 base entity.""" + def __init__(self, coordinator: IstaDataUpdateCoordinator, controller: PyEcotrendIsta, uuid: str) -> None: + """Initialize the ista EcoTrend Version 3 base entity.""" super().__init__(coordinator) self._attr_attribution = f"Data provided by {URL_SELECTORS.get(self.coordinator.config_entry.options.get(CONF_URL))}" self._support_code = controller._supportCode + self.uuid = uuid self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, f"{self._support_code}")}, - manufacturer=f"{MANUFACTURER} {self._support_code}", + identifiers={(DOMAIN, f"{self.uuid}")}, + manufacturer=f"{MANUFACTURER} {self.uuid}", model="ista consumption & costs", - name=f"{DEVICE_NAME} {self._support_code} {'' if controller._accessToken != 'Demo' else 'Demo'}", + name=f"{DEVICE_NAME} {self.uuid} {'' if controller._accessToken != 'Demo' else 'Demo'}", sw_version=controller.getVersion(), hw_version=controller._a_tosUpdated, - via_device=(DOMAIN, f"{self._support_code}"), + via_device=(DOMAIN, f"{self.uuid}"), ) self._unsub_dispatchers: list[Callable[[], None]] = [] @@ -75,25 +76,26 @@ async def update(self): _LOGGER.debug("update data in Coordinator") -class EcotrendSensorV2(EcotrendBaseEntityV2, SensorEntity): - """Sensor entity class for ista EcoTrend Version 2.""" +class EcotrendSensorV3(EcotrendBaseEntityV3, SensorEntity): + """Sensor entity class for ista EcoTrend Version 3.""" def __init__( self, coordinator: IstaDataUpdateCoordinator, controller: PyEcotrendIsta, - last: dict[str, any], + last: dict[str, Any], description: EcotrendSensorEntityDescription, + uuid: str, ) -> None: - """Initialize the ista EcoTrend Version 2 sensor.""" + """Initialize the ista EcoTrend Version 3 sensor.""" self.entity_description = description - super().__init__(coordinator, controller) + super().__init__(coordinator, controller, uuid) if not last: return - self._attr_name: str = f"{description.key}_{self._support_code}".replace("_", " ").title() - self._attr_unique_id = f"{description.key}-{self._support_code}" + self._attr_name: str = f"{description.key}_{self.uuid}".replace("_", " ").title() + self._attr_unique_id = f"{description.key}_{self.uuid}" self.consum_value = last.get(description.data_type) if description.costs_or_cosums == "costs": self._attr_native_unit_of_measurement = last.get("unit", None) # Währung @@ -117,8 +119,8 @@ def native_value(self) -> StateType: def extra_state_attributes(self) -> dict[str, Any]: """Return the extra state attributes of the sensor.""" data = super().extra_state_attributes or {} - if self.coordinator.data: - return dict(data, **self.coordinator.data.to_dict()) + if self.coordinator.data[self.uuid]: + return dict(data, **self.coordinator.data[self.uuid].to_dict()) return dict(data, **{}) @@ -127,47 +129,59 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the ista EcoTrend Version 2 sensors from the config entry.""" + """Set up the ista EcoTrend Version 3 sensors from the config entry.""" coordinator: IstaDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] controller = coordinator.controller - entities: list = [] - consum_raw: CustomRaw = CustomRaw.from_dict( - await hass.async_add_executor_job( - controller.consum_raw, - [ - datetime.datetime.now().year, - datetime.datetime.now().year - 1, - ], + for uuid in controller.getUUIDs(): + entities: list = [] + consum_raw: CustomRaw = CustomRaw.from_dict( + await hass.async_add_executor_job( + controller.consum_raw, + [ + datetime.datetime.now().year, + datetime.datetime.now().year - 1, + ], + None, + True, + uuid, + ) ) - ) - consum_dict = consum_raw.to_dict() - last_value = consum_dict.get("last_value", None) - last_custom_value = consum_dict.get("last_custom_value", None) - last_costs = consum_dict.get("last_costs", None) - - for description in SENSOR_TYPES: - descr: EcotrendSensorEntityDescription = description - if not hasattr(consum_raw, "consum_types") or not consum_raw.consum_types: - continue - for consum_type in consum_raw.consum_types: - if descr.data_type != consum_type: + consum_dict = consum_raw.to_dict() + last_value = consum_dict.get("last_value", None) + last_custom_value = consum_dict.get("last_custom_value", None) + last_costs = consum_dict.get("last_costs", None) + for description in SENSOR_TYPES: + descr: EcotrendSensorEntityDescription = description + if not hasattr(consum_raw, "consum_types") or not consum_raw.consum_types: continue - if descr.costs_or_cosums == "consums" and (last_value or last_custom_value): - entities.append( - EcotrendSensorV2( - coordinator, - controller, - ( - last_custom_value - if descr.key in ("warmwater_custom", "water_custom", "heating_custom") - else last_value - ), - descr, + for consum_type in consum_raw.consum_types: + if descr.data_type != consum_type: + continue + if descr.costs_or_cosums == "consums" and (last_value or last_custom_value): + entities.append( + EcotrendSensorV3( + coordinator, + controller, + ( + last_custom_value + if descr.key in ("warmwater_custom", "water_custom", "heating_custom") + else last_value + ), + descr, + uuid, + ) + ) + elif descr.costs_or_cosums == "costs" and last_costs: + entities.append( + EcotrendSensorV3( + coordinator, + controller, + last_costs, + descr, + uuid, + ) ) - ) - elif descr.costs_or_cosums == "costs" and last_costs: - entities.append(EcotrendSensorV2(coordinator, controller, last_costs, descr)) async_add_entities(entities)