Skip to content

Commit

Permalink
Implement service event sensor with history (#506)
Browse files Browse the repository at this point in the history
* Implement service event sensor with history

Inspired by operations history sensor.
  • Loading branch information
fursov authored Jan 5, 2025
1 parent 11adb9e commit 0a1360c
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 25 deletions.
1 change: 1 addition & 0 deletions custom_components/myskoda/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 16 additions & 3 deletions custom_components/myskoda/coordinator.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -21,6 +21,7 @@
EventAirConditioning,
EventDeparture,
EventOperation,
ServiceEvent,
ServiceEventTopic,
)
from myskoda.models.info import CapabilityId
Expand All @@ -39,6 +40,7 @@
DEFAULT_FETCH_INTERVAL_IN_MINUTES,
DOMAIN,
MAX_STORED_OPERATIONS,
MAX_STORED_SERVICE_EVENTS,
)
from .error_handlers import handle_aiohttp_error

Expand Down Expand Up @@ -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
Expand All @@ -81,6 +87,7 @@ class State:
user: User
config: Config
operations: Operations
service_events: ServiceEvents


class MySkodaDataUpdateCoordinator(DataUpdateCoordinator[State]):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion custom_components/myskoda/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 11 additions & 8 deletions custom_components/myskoda/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
},
"window_open_rear_right": {
"default": "mdi:car-door"
}
}
},
"climate": {
"climate": {
Expand All @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -271,4 +274,4 @@
}
}
}
}
}
42 changes: 42 additions & 0 deletions custom_components/myskoda/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ async def async_setup_entry(
OutsideTemperature,
Range,
RemainingChargingTime,
ServiceEvent,
SoftwareVersion,
TargetBatteryPercentage,
],
Expand Down Expand Up @@ -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."""

Expand Down
29 changes: 16 additions & 13 deletions custom_components/myskoda/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down Expand Up @@ -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"
Expand All @@ -269,9 +272,9 @@
"electric_range": {
"name": "Electric Range"
},
"outside_temperature": {
"name": "Outside Temperature"
}
"outside_temperature": {
"name": "Outside Temperature"
}
},
"switch": {
"battery_care_mode": {
Expand Down Expand Up @@ -360,4 +363,4 @@
}
}
}
}
}

0 comments on commit 0a1360c

Please sign in to comment.