From 893043e4ce3554c6f8f2fe4b8e8b023c14f017b3 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 4 Apr 2025 18:07:03 +0200 Subject: [PATCH] feat: grab all collection events in 1 call and dispatch in several sensors after It becomes a bit of a mess with a lot of dynamically named sensors, and possibly a lot of empty ones. It adds a bit of a burden on the user to clean-up the mess. :/ --- custom_components/ecocito/__init__.py | 35 +-- custom_components/ecocito/client.py | 33 ++- custom_components/ecocito/coordinator.py | 13 +- custom_components/ecocito/manifest.json | 5 +- custom_components/ecocito/sensor.py | 223 +++++++----------- .../ecocito/translations/en.json | 10 +- requirements.txt | 1 + 7 files changed, 138 insertions(+), 182 deletions(-) diff --git a/custom_components/ecocito/__init__.py b/custom_components/ecocito/__init__.py index 29b9fe9..07afc3a 100644 --- a/custom_components/ecocito/__init__.py +++ b/custom_components/ecocito/__init__.py @@ -27,11 +27,9 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] class EcocitoData: """Ecocito data type.""" - garbage_collections: CollectionDataUpdateCoordinator | None - garbage_collections_previous: CollectionDataUpdateCoordinator | None - recycling_collections: CollectionDataUpdateCoordinator | None - recycling_collections_previous: CollectionDataUpdateCoordinator | None - waste_depot_visits: WasteDepotVisitsDataUpdateCoordinator # Maybe we could have an optional here if we had some checkbox config? + collection_types: dict[int, str] + collections: CollectionDataUpdateCoordinator + waste_depot_visits: WasteDepotVisitsDataUpdateCoordinator type EcocitoConfigEntry = ConfigEntry[EcocitoData] @@ -46,31 +44,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: EcocitoConfigEntry) -> b ) await client.authenticate() - garbage_id = entry.data.get(ECOCITO_GARBAGE_TYPE) - recycle_id = entry.data.get(ECOCITO_RECYCLE_TYPE) refresh_time = entry.data.get(ECOCITO_REFRESH_MIN_KEY, ECOCITO_DEFAULT_REFRESH_MIN) + collect_types = await client.get_collection_types() + data = EcocitoData( - garbage_collections=CollectionDataUpdateCoordinator( - hass, client, 0, garbage_id, refresh_time - ) if garbage_id is not None else None, - garbage_collections_previous=CollectionDataUpdateCoordinator( - hass, client, -1, garbage_id, refresh_time - ) if garbage_id is not None else None, - recycling_collections=CollectionDataUpdateCoordinator( - hass, client, 0, recycle_id, refresh_time - ) if recycle_id is not None else None, - recycling_collections_previous=CollectionDataUpdateCoordinator( - hass, client, -1, recycle_id, refresh_time - ) if recycle_id is not None else None, + collection_types = collect_types, + collections = CollectionDataUpdateCoordinator( + hass, client, refresh_time + ), waste_depot_visits=WasteDepotVisitsDataUpdateCoordinator( hass, client, 0, refresh_time ), ) - for field in fields(data): - coordinator = getattr(data, field.name) - if coordinator: - await coordinator.async_config_entry_first_refresh() + await data.collections.async_config_entry_first_refresh() + await data.waste_depot_visits.async_config_entry_first_refresh() + entry.runtime_data = data await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/custom_components/ecocito/client.py b/custom_components/ecocito/client.py index eef9b0b..d6719bb 100644 --- a/custom_components/ecocito/client.py +++ b/custom_components/ecocito/client.py @@ -106,8 +106,8 @@ class EcocitoClient: ) from e async def get_collection_events( - self, event_type: str, year: int - ) -> list[CollectionEvent]: + self, year: int + ) -> dict[str, dict[str, CollectionEvent]]: """Return the list of the collection events for a type and a year.""" async with aiohttp.ClientSession(cookie_jar=self._cookies) as session: try: @@ -119,8 +119,8 @@ class EcocitoClient: "skip": "0", "take": "1000", "requireTotalCount": "true", - "idMatiere": str(event_type), - "dateDebut": f"{year}-01-01T00:00:00.000Z", + "idMatiere": str(-1), + "dateDebut": f"{year - 1}-01-01T00:00:00.000Z", "dateFin": f"{year}-12-31T23:59:59.999Z", }, raise_for_status=True, @@ -128,15 +128,24 @@ class EcocitoClient: content = await response.text() try: - return [ - CollectionEvent( - type=event_type, - date=datetime.fromisoformat(row["DATE_DONNEE"]), - location=row["LIBELLE_ADRESSE"], - quantity=row["QUANTITE_NETTE"], + result = {} + for row in json.loads(content).get("data", []): + date = datetime.fromisoformat(row["DATE_DONNEE"]) + y = "current" if date.year == year else "last" + matter = row["ID_MATIERE"] + if y not in result: + result[y] = {} + if matter not in result[y]: + result[y][matter] = [] + result[y][matter].append( + CollectionEvent( + type=matter, + date=date, + location=row["LIBELLE_ADRESSE"], + quantity=row["QUANTITE_NETTE"], + ) ) - for row in json.loads(content).get("data", []) - ] + return result except Exception as e: # noqa: BLE001 await self._handle_error(content, e) diff --git a/custom_components/ecocito/coordinator.py b/custom_components/ecocito/coordinator.py index 59e312d..9b0f114 100644 --- a/custom_components/ecocito/coordinator.py +++ b/custom_components/ecocito/coordinator.py @@ -64,21 +64,18 @@ class CollectionDataUpdateCoordinator( self, hass: HomeAssistant, client: EcocitoClient, - year_offset: int, - coll_type_id: int, refresh_time: int ) -> None: """Initialize the coordinator.""" + self.cached: dict[str, dict[str, list[CollectionEvent]]] | None = None super().__init__(hass, client, refresh_time) - self._year_offset = year_offset - self._coll_type_id = coll_type_id async def _fetch_data(self) -> list[CollectionEvent]: """Fetch the data.""" - return await self.client.get_collection_events( - str(self._coll_type_id), - datetime.now(tz=self._time_zone).year + self._year_offset, - ) + if self.cached is None: + self.cached = await self.client.get_collection_events(datetime.now(tz=self._time_zone).year) + + return self.cached class WasteDepotVisitsDataUpdateCoordinator( diff --git a/custom_components/ecocito/manifest.json b/custom_components/ecocito/manifest.json index f3dd117..92af7ae 100644 --- a/custom_components/ecocito/manifest.json +++ b/custom_components/ecocito/manifest.json @@ -9,5 +9,8 @@ "issue_tracker": "https://github.com/rclsilver/home-assistant-ecocito/issues", "loggers": ["custom_components.ecocito"], "single_config_entry": true, - "version": "0.0.0" + "version": "0.0.0", + "requirements": [ + "unidecode>=1.3.8" + ] } diff --git a/custom_components/ecocito/sensor.py b/custom_components/ecocito/sensor.py index c4dc3b1..0bb7c1b 100644 --- a/custom_components/ecocito/sensor.py +++ b/custom_components/ecocito/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations import dataclasses +import unidecode from collections.abc import Callable from datetime import datetime from typing import Any, Generic @@ -18,21 +19,25 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EcocitoConfigEntry from .client import CollectionEvent, EcocitoEvent -from .const import DEVICE_ATTRIBUTION +from .const import DEVICE_ATTRIBUTION, LOGGER from .coordinator import T from .entity import EcocitoEntity def get_count(data: list[Any]) -> int: """Return the size of the given list.""" - return len(data) + if data: + return len(data) + else: + return 0 def get_event_collections_weight(data: list[CollectionEvent]) -> int: """Return the sum of the events quantities.""" result = 0 - for row in data: - result += row.quantity + if data: + for row in data: + result += row.quantity return result @@ -59,152 +64,91 @@ class EcocitoSensorEntityDescription(SensorEntityDescription, Generic[T]): def __init__( self, + year: str | None, + mat_type: str | None, value_fn: Callable[[T], str | int], last_updated_fn: Callable[[T], datetime], *args: tuple, **kwargs: dict[str, any], ) -> None: """Build a Ecocito sensor.""" + self.mat_type = mat_type + self.year = year self.value_fn = value_fn self.last_updated_fn = last_updated_fn super().__init__(*args, **kwargs) +def cleanup_name(matter_name) -> str: + matter_name = str(matter_name).replace(" ", "_") + return unidecode.unidecode(matter_name, "utf-8").lower() -SENSOR_TYPES: tuple[tuple[str, EcocitoSensorEntityDescription]] = ( - ( - "garbage_collections", - EcocitoSensorEntityDescription( - key="garbage_collections_count", - translation_key="garbage_collections_count", - value_fn=get_count, - last_updated_fn=get_latest_date, - icon="mdi:trash-can", - state_class=SensorStateClass.TOTAL, - ), - ), - ( - "garbage_collections", - EcocitoSensorEntityDescription( - key="garbage_collections_total", - translation_key="garbage_collections_total", - icon="mdi:trash-can", - value_fn=get_event_collections_weight, - last_updated_fn=get_latest_date, - unit_of_measurement=UnitOfMass.KILOGRAMS, - state_class=SensorStateClass.TOTAL, - suggested_display_precision=0, - ), - ), - ( - "garbage_collections", - EcocitoSensorEntityDescription( - key="latest_garbage_collections", - translation_key="latest_garbage_collection", - icon="mdi:trash-can", - value_fn=get_latest_event_collection_weight, - last_updated_fn=get_latest_date, - unit_of_measurement=UnitOfMass.KILOGRAMS, - state_class=SensorStateClass.TOTAL, - suggested_display_precision=0, - ), - ), - ( - "garbage_collections_previous", - EcocitoSensorEntityDescription( - key="garbage_collections_count_previous", - translation_key="previous_garbage_collections_count", - icon="mdi:trash-can", - value_fn=get_count, - last_updated_fn=get_latest_date, - state_class=SensorStateClass.TOTAL, - ), - ), - ( - "garbage_collections_previous", - EcocitoSensorEntityDescription( - key="garbage_collections_total_previous", - translation_key="previous_garbage_collections_total", - icon="mdi:trash-can", - value_fn=get_event_collections_weight, - last_updated_fn=get_latest_date, - unit_of_measurement=UnitOfMass.KILOGRAMS, - state_class=SensorStateClass.TOTAL, - suggested_display_precision=0, - ), - ), - ( - "recycling_collections", - EcocitoSensorEntityDescription( - key="recycling_collections_count", - translation_key="recycling_collections_count", - icon="mdi:recycle", - value_fn=get_count, - last_updated_fn=get_latest_date, - state_class=SensorStateClass.TOTAL, - ), - ), - ( - "recycling_collections", - EcocitoSensorEntityDescription( - key="recycling_collections_total", - translation_key="recycling_collections_total", - icon="mdi:recycle", - value_fn=get_event_collections_weight, - last_updated_fn=get_latest_date, - unit_of_measurement=UnitOfMass.KILOGRAMS, - state_class=SensorStateClass.TOTAL, - suggested_display_precision=0, - ), - ), - ( - "recycling_collections", - EcocitoSensorEntityDescription( - key="latest_recycling_collections", - translation_key="latest_recycling_collection", - icon="mdi:recycle", - value_fn=get_latest_event_collection_weight, - last_updated_fn=get_latest_date, - unit_of_measurement=UnitOfMass.KILOGRAMS, - state_class=SensorStateClass.TOTAL, - suggested_display_precision=0, - ), - ), - ( - "recycling_collections_previous", - EcocitoSensorEntityDescription( - key="recycling_collections_count_previous", - translation_key="previous_recycling_collections_count", - icon="mdi:recycle", - value_fn=get_count, - last_updated_fn=get_latest_date, - state_class=SensorStateClass.TOTAL, - ), - ), - ( - "recycling_collections_previous", - EcocitoSensorEntityDescription( - key="recycling_collections_total_previous", - translation_key="previous_recycling_collections_total", - icon="mdi:recycle", - value_fn=get_event_collections_weight, - last_updated_fn=get_latest_date, - unit_of_measurement=UnitOfMass.KILOGRAMS, - state_class=SensorStateClass.TOTAL, - suggested_display_precision=0, - ), - ), - ( +def build_sensor_types(mat_types) -> list[tuple[str, EcocitoSensorEntityDescription]]: + sensors = [] + for mat_type, mat_type_name in mat_types.items(): + if int(mat_type) == -1: + continue + name = cleanup_name(str(mat_type_name)) + for year in ["current", "last"]: + sensors.append(( + "collections", + EcocitoSensorEntityDescription( + name=f"{name}_collections_count_{year}_year", + key=f"{name}_collections_count_{year}_year", + # translation_key=f"garbage_collections_count_{year}_year", + year=year, + mat_type=mat_type, + value_fn=get_count, + last_updated_fn=get_latest_date, + icon="mdi:trash-can", + state_class=SensorStateClass.TOTAL, + ), + )) + sensors.append(( + "collections", + EcocitoSensorEntityDescription( + name=f"{name}_collections_total_{year}_year", + key=f"{name}_collections_total_{year}_year", + # translation_key=f"garbage_collections_total_{year}_year", + icon="mdi:trash-can", + year=year, + mat_type=mat_type, + value_fn=get_event_collections_weight, + last_updated_fn=get_latest_date, + unit_of_measurement=UnitOfMass.KILOGRAMS, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + ), + )) + sensors.append(( + "collections", + EcocitoSensorEntityDescription( + key=f"latest_{name}_collection", + name=f"latest_{name}_collection", + # translation_key=f"latest_garbage_collection", + icon="mdi:trash-can", + year=year, + mat_type=mat_type, + value_fn=get_latest_event_collection_weight, + last_updated_fn=get_latest_date, + unit_of_measurement=UnitOfMass.KILOGRAMS, + state_class=SensorStateClass.TOTAL, + suggested_display_precision=0, + ), + )) + sensors.append(( "waste_depot_visits", EcocitoSensorEntityDescription( key="waste_deposit_visit", translation_key="waste_deposit_visit", icon="mdi:car", + year=None, + mat_type=None, value_fn=get_count, last_updated_fn=get_latest_date, state_class=SensorStateClass.TOTAL, ), - ), -) + )) + return sensors class EcocitoSensor(EcocitoEntity[T], SensorEntity): @@ -216,14 +160,25 @@ class EcocitoSensor(EcocitoEntity[T], SensorEntity): @property def native_value(self) -> str | int: """Return the state of the sensor.""" - return self.entity_description.value_fn(self.coordinator.data) + data = self.coordinator.data + if self.entity_description.year is not None: + data = data.get(self.entity_description.year) + if self.entity_description.mat_type is not None: + data = data.get(self.entity_description.mat_type) + + return self.entity_description.value_fn(data) @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the state attributes of the sensor.""" + data = self.coordinator.data + if self.entity_description.year is not None: + data = data.get(self.entity_description.year) + if self.entity_description.mat_type is not None: + data = data.get(self.entity_description.mat_type) return { "last_updated": self.entity_description.last_updated_fn( - self.coordinator.data + data ), } @@ -234,8 +189,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Ecocito sensors based on a config entry.""" + entities: list[EcocitoSensor[Any]] = [] - for coordinator_type, description in SENSOR_TYPES: + mat_types = entry.runtime_data.collection_types + for coordinator_type, description in build_sensor_types(mat_types): coordinator = getattr(entry.runtime_data, coordinator_type) if coordinator: entities.append(EcocitoSensor(coordinator, description)) diff --git a/custom_components/ecocito/translations/en.json b/custom_components/ecocito/translations/en.json index 9d09cff..004b56e 100644 --- a/custom_components/ecocito/translations/en.json +++ b/custom_components/ecocito/translations/en.json @@ -20,19 +20,19 @@ }, "entity": { "sensor": { - "garbage_collections_count": { + "garbage_collections_count_current_year": { "name": "Number of garbage collections" }, - "garbage_collections_total": { + "garbage_collections_total_current_year": { "name": "Total weight of collected garbage" }, - "latest_garbage_collection": { + "latest_garbage_collection_current_year": { "name": "Weight of the latest garbage collection" }, - "previous_garbage_collections_count": { + "garbage_collections_count_last_year": { "name": "Number of garbage collections (last year)" }, - "previous_garbage_collections_total": { + "garbage_collections_total_last_year": { "name": "Total weight of collected garbage (last year)" }, "recycling_collections_count": { diff --git a/requirements.txt b/requirements.txt index 679baee..16074bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ colorlog==6.8.2 homeassistant==2025.3.4 pip>=21.3.1 ruff==0.11.0 +unidecode>=1.3.8