Skip to content

Commit

Permalink
✨ 3.0.0 Beta - Several residential units (#166)
Browse files Browse the repository at this point in the history
* ✨ 3.0.0 Beta - Several residential units

Signed-off-by: Ludy87 <Ludy87@users.noreply.github.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Signed-off-by: Ludy87 <Ludy87@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
Ludy87 and pre-commit-ci[bot] authored Jan 27, 2024
1 parent 21a5738 commit 4735006
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 87 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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

Expand Down
50 changes: 45 additions & 5 deletions custom_components/ecotrend_ista/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""ista EcoTrend Version 2."""
"""ista EcoTrend Version 3."""
from __future__ import annotations

import logging
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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, {})
Expand All @@ -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
6 changes: 3 additions & 3 deletions custom_components/ecotrend_ista/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Config flow for ista EcoTrend Version 2."""
"""Config flow for ista EcoTrend Version 3."""
from __future__ import annotations

import copy
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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] = "*****"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ecotrend_ista/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Const for ista EcoTrend Version 2."""
"""Const for ista EcoTrend Version 3."""
from __future__ import annotations

from typing import Final
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ecotrend_ista/const_schema.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
48 changes: 27 additions & 21 deletions custom_components/ecotrend_ista/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Coordinator for ista EcoTrend Version 2."""
"""Coordinator for ista EcoTrend Version 3."""
from __future__ import annotations

import datetime
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions custom_components/ecotrend_ista/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
118 changes: 66 additions & 52 deletions custom_components/ecotrend_ista/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]] = []

Expand All @@ -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
Expand All @@ -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, **{})


Expand All @@ -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)

0 comments on commit 4735006

Please sign in to comment.