From 0a1360cb54d6ef703fee143b23bf35dfa541fdf7 Mon Sep 17 00:00:00 2001 From: Dmitry Fursov <2463426+fursov@users.noreply.github.com> Date: Sun, 5 Jan 2025 23:15:43 +0200 Subject: [PATCH] Implement service event sensor with history (#506) * Implement service event sensor with history Inspired by operations history sensor. --- custom_components/myskoda/const.py | 1 + custom_components/myskoda/coordinator.py | 19 +++++++-- custom_components/myskoda/entity.py | 9 +++- custom_components/myskoda/icons.json | 19 +++++---- custom_components/myskoda/sensor.py | 42 +++++++++++++++++++ .../myskoda/translations/en.json | 29 +++++++------ 6 files changed, 94 insertions(+), 25 deletions(-) diff --git a/custom_components/myskoda/const.py b/custom_components/myskoda/const.py index 6c5b9e6..79fe5b9 100644 --- a/custom_components/myskoda/const.py +++ b/custom_components/myskoda/const.py @@ -13,6 +13,7 @@ CONF_READONLY = "readonly" MAX_STORED_OPERATIONS = 2 +MAX_STORED_SERVICE_EVENTS = 2 OUTSIDE_TEMP_MIN_BOUND = -50 OUTSIDE_TEMP_MAX_BOUND = 60 diff --git a/custom_components/myskoda/coordinator.py b/custom_components/myskoda/coordinator.py index 9f763fa..a69cb26 100644 --- a/custom_components/myskoda/coordinator.py +++ b/custom_components/myskoda/coordinator.py @@ -1,6 +1,6 @@ import asyncio import logging -from collections import OrderedDict +from collections import OrderedDict, deque from collections.abc import Coroutine from dataclasses import dataclass from datetime import timedelta @@ -21,6 +21,7 @@ EventAirConditioning, EventDeparture, EventOperation, + ServiceEvent, ServiceEventTopic, ) from myskoda.models.info import CapabilityId @@ -39,6 +40,7 @@ DEFAULT_FETCH_INTERVAL_IN_MINUTES, DOMAIN, MAX_STORED_OPERATIONS, + MAX_STORED_SERVICE_EVENTS, ) from .error_handlers import handle_aiohttp_error @@ -70,6 +72,10 @@ def __init__( Operations = OrderedDict[str, EventOperation] +# History of EventType.SERVICE_EVENT events +ServiceEvents = deque[ServiceEvent] + + @dataclass class Config: auxiliary_heater_duration: float | None = None @@ -81,6 +87,7 @@ class State: user: User config: Config operations: Operations + service_events: ServiceEvents class MySkodaDataUpdateCoordinator(DataUpdateCoordinator[State]): @@ -111,6 +118,7 @@ def __init__( self.vin: str = vin self.myskoda: MySkoda = myskoda self.operations: OrderedDict = OrderedDict() + self.service_events: deque = deque(maxlen=MAX_STORED_SERVICE_EVENTS) self.entry: ConfigEntry = entry self.update_driving_range = self._debounce(self._update_driving_range) self.update_charging = self._debounce(self._update_charging) @@ -156,6 +164,7 @@ async def _async_update_data(self) -> State: user = None config = self.data.config if self.data and self.data.config else Config() operations = self.operations + service_events = self.service_events if self.entry.state == ConfigEntryState.SETUP_IN_PROGRESS: if getattr(self, "_startup_called", False): @@ -192,7 +201,7 @@ async def _async_finish_startup(hass: HomeAssistant) -> None: async_at_started( hass=self.hass, at_start_cb=_async_finish_startup ) # Schedule post-setup tasks - return State(vehicle, user, config, operations) + return State(vehicle, user, config, operations, service_events) # Regular update @@ -217,7 +226,7 @@ async def _async_finish_startup(hass: HomeAssistant) -> None: raise UpdateFailed("Error getting update from MySkoda API: %s", err) if vehicle and user: - return State(vehicle, user, config, operations) + return State(vehicle, user, config, operations, service_events) raise UpdateFailed("Incomplete update received") async def _mqtt_connect(self) -> None: @@ -238,6 +247,10 @@ async def _on_mqtt_event(self, event: Event) -> None: if event.type == EventType.OPERATION: await self._on_operation_event(event) if event.type == EventType.SERVICE_EVENT: + # Store the event and update data + self.service_events.appendleft(event.event) + self.async_set_updated_data(self.data) + if event.topic == ServiceEventTopic.CHARGING: await self._on_charging_event(event) if event.topic == ServiceEventTopic.ACCESS: diff --git a/custom_components/myskoda/entity.py b/custom_components/myskoda/entity.py index 520bef4..2ee9136 100644 --- a/custom_components/myskoda/entity.py +++ b/custom_components/myskoda/entity.py @@ -8,7 +8,10 @@ from myskoda.models.info import CapabilityId from .const import DOMAIN -from .coordinator import MySkodaDataUpdateCoordinator +from .coordinator import ( + MySkodaDataUpdateCoordinator, + ServiceEvents, +) class MySkodaEntity(CoordinatorEntity): @@ -36,6 +39,10 @@ def vehicle(self) -> Vehicle: def operations(self) -> dict[str, EventOperation]: return self.coordinator.data.operations + @property + def service_events(self) -> ServiceEvents: + return self.coordinator.data.service_events + @property def device_info(self) -> DeviceInfo: # noqa: D102 return { diff --git a/custom_components/myskoda/icons.json b/custom_components/myskoda/icons.json index d629d49..db42e4f 100644 --- a/custom_components/myskoda/icons.json +++ b/custom_components/myskoda/icons.json @@ -66,7 +66,7 @@ }, "window_open_rear_right": { "default": "mdi:car-door" - } + } }, "climate": { "climate": { @@ -85,9 +85,9 @@ "charge_limit": { "default": "mdi:battery-lock" }, - "auxiliary_heater_duration": { - "default": "mdi:timer" - } + "auxiliary_heater_duration": { + "default": "mdi:timer" + } }, "image": { "render_url_main": { @@ -163,15 +163,18 @@ "operation": { "default": "mdi:cog-transfer" }, + "service_event": { + "default": "mdi:message-cog-outline" + }, "software_version": { "default": "mdi:update" }, "target_battery_percentage": { "default": "mdi:percent" }, - "outside_temperature": { - "default": "mdi:thermometer" - } + "outside_temperature": { + "default": "mdi:thermometer" + } }, "switch": { "battery_care_mode": { @@ -271,4 +274,4 @@ } } } -} +} \ No newline at end of file diff --git a/custom_components/myskoda/sensor.py b/custom_components/myskoda/sensor.py index c31f921..bad7100 100644 --- a/custom_components/myskoda/sensor.py +++ b/custom_components/myskoda/sensor.py @@ -63,6 +63,7 @@ async def async_setup_entry( OutsideTemperature, Range, RemainingChargingTime, + ServiceEvent, SoftwareVersion, TargetBatteryPercentage, ], @@ -130,6 +131,47 @@ def extra_state_attributes(self) -> dict: return attributes +class ServiceEvent(MySkodaSensor): + """Report the most recent service event.""" + + entity_description = SensorEntityDescription( + key="service_event", + translation_key="service_event", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + ) + + @property + def native_value(self) -> datetime | None: + """Returns the timestamp of the last seen service event.""" + if self.service_events: + last_service_event = self.service_events[0] + return last_service_event.timestamp + + @property + def extra_state_attributes(self) -> dict: + """Returns additional attributes for the service event sensor. + + - history: a list of dicts with the same fields for the previously seen event. + """ + attributes = {} + if not self.service_events: + return attributes + + filtered = [ + { + "name": event.name.value, + "timestamp": event.timestamp, + "data": event.data, + } + for event in self.service_events + ] + attributes = filtered[0] + attributes["history"] = filtered[1:] + + return attributes + + class SoftwareVersion(MySkodaSensor): """Current software version of a vehicle.""" diff --git a/custom_components/myskoda/translations/en.json b/custom_components/myskoda/translations/en.json index 12c8d8d..e383007 100644 --- a/custom_components/myskoda/translations/en.json +++ b/custom_components/myskoda/translations/en.json @@ -165,9 +165,9 @@ } }, "image": { - "render_light_3x": { - "name": "Light Status Render of Vehicle" - }, + "render_light_3x": { + "name": "Light Status Render of Vehicle" + }, "render_vehicle_main": { "name": "Main Render of Vehicle" } @@ -238,12 +238,15 @@ }, "operation": { "name": "Last Operation", - "state": { - "in_progress": "In Progress", - "completed_success": "Completed succesfully", - "completed_warning": "Completed with warning", - "error": "Error" - } + "state": { + "in_progress": "In Progress", + "completed_success": "Completed succesfully", + "completed_warning": "Completed with warning", + "error": "Error" + } + }, + "service_event": { + "name": "Last Service Event" }, "software_version": { "name": "Software Version" @@ -269,9 +272,9 @@ "electric_range": { "name": "Electric Range" }, - "outside_temperature": { - "name": "Outside Temperature" - } + "outside_temperature": { + "name": "Outside Temperature" + } }, "switch": { "battery_care_mode": { @@ -360,4 +363,4 @@ } } } -} +} \ No newline at end of file