From 5fd94a28dccaf6deb91f1e7d62ef9fe99a555ba7 Mon Sep 17 00:00:00 2001 From: Nils Vogels Date: Wed, 11 Dec 2024 13:35:48 +0100 Subject: [PATCH 1/7] Add RenderImage class that sensors can subclass from --- custom_components/myskoda/image.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/custom_components/myskoda/image.py b/custom_components/myskoda/image.py index 746c38c..36b425f 100644 --- a/custom_components/myskoda/image.py +++ b/custom_components/myskoda/image.py @@ -1,10 +1,12 @@ """Images for the MySkoda integration.""" +import httpx import logging from homeassistant.components.image import ( ImageEntity, ImageEntityDescription, + GET_IMAGE_TIMEOUT, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -59,6 +61,34 @@ def __init__( super().__init__(coordinator, vin) +class RenderImage(MySkodaImage): + async def _fetch_url(self, url: str) -> httpx.Response | None: + """Fetch a URL passing in the MySkoda access token.""" + + try: + response = await self._client.get( + url, + timeout=GET_IMAGE_TIMEOUT, + follow_redirects=True, + headers={ + "authorization": f"Bearer {await self.coordinator.myskoda.authorization.get_access_token()}" + }, + ) + response.raise_for_status() + except httpx.TimeoutException: + _LOGGER.error("%s: Timeout getting image from %s", self.entity_id, url) + return None + except (httpx.RequestError, httpx.HTTPStatusError) as err: + _LOGGER.error( + "%s: Error getting new image from %s: %s", + self.entity_id, + url, + err, + ) + return None + return response + + class MainRenderImage(MySkodaImage): """Main render of the vehicle.""" From 7a59d221a06684ef1f9c151ee7dda3efa277a423 Mon Sep 17 00:00:00 2001 From: Nils Vogels Date: Wed, 11 Dec 2024 16:13:46 +0100 Subject: [PATCH 2/7] Rename RenderImage to StatusImage Implement 2 images, default disabled --- custom_components/myskoda/image.py | 40 ++++++++++++++++++- .../myskoda/translations/en.json | 6 +++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/custom_components/myskoda/image.py b/custom_components/myskoda/image.py index 36b425f..9c869d1 100644 --- a/custom_components/myskoda/image.py +++ b/custom_components/myskoda/image.py @@ -33,7 +33,11 @@ async def async_setup_entry( entities = [] for vin in hass.data[DOMAIN][config.entry_id][COORDINATORS]: - for SensorClass in [MainRenderImage]: + for SensorClass in [ + MainRenderImage, + DarkStatusImage, + LightStatusImage, + ]: entities.append( SensorClass( hass.data[DOMAIN][config.entry_id][COORDINATORS][vin], vin, hass @@ -61,7 +65,9 @@ def __init__( super().__init__(coordinator, vin) -class RenderImage(MySkodaImage): +class StatusImage(MySkodaImage): + """A render of the current status of the vehicle.""" + async def _fetch_url(self, url: str) -> httpx.Response | None: """Fetch a URL passing in the MySkoda access token.""" @@ -116,3 +122,33 @@ def extra_state_attributes(self) -> dict: for r in composite_renders: attributes["composite_renders"][r] = composite_renders[r] return attributes + + +class LightStatusImage(StatusImage): + """Light 3x render of the vehicle status.""" + + entity_description = ImageEntityDescription( + key="render_light_3x", + translation_key="render_light_3x", + entity_registry_enabled_default=False, + ) + + @property + def image_url(self) -> str | None: + if status := self.vehicle.status: + return status.renders.light_mode.three_x + + +class DarkStatusImage(StatusImage): + """Dark 3x render of the vehicle status.""" + + entity_description = ImageEntityDescription( + key="render_dark_3x", + translation_key="render_dark_3x", + entity_registry_enabled_default=False, + ) + + @property + def image_url(self) -> str | None: + if status := self.vehicle.status: + return status.renders.dark_mode.three_x diff --git a/custom_components/myskoda/translations/en.json b/custom_components/myskoda/translations/en.json index 315fd42..78c7fe9 100644 --- a/custom_components/myskoda/translations/en.json +++ b/custom_components/myskoda/translations/en.json @@ -109,6 +109,12 @@ } }, "image": { + "render_dark_3x": { + "name": "Dark Status Render of Vehicle" + }, + "render_light_3x": { + "name": "Light Status Render of Vehicle" + }, "render_vehicle_main": { "name": "Main Render of Vehicle" } From 17ffa862f52508cedc93a09087169f36b9dc1f2a Mon Sep 17 00:00:00 2001 From: Nils Vogels Date: Wed, 11 Dec 2024 23:02:19 +0100 Subject: [PATCH 3/7] Remove Dark status image as both are identical --- custom_components/myskoda/image.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/custom_components/myskoda/image.py b/custom_components/myskoda/image.py index 9c869d1..aac9d99 100644 --- a/custom_components/myskoda/image.py +++ b/custom_components/myskoda/image.py @@ -35,7 +35,6 @@ async def async_setup_entry( for vin in hass.data[DOMAIN][config.entry_id][COORDINATORS]: for SensorClass in [ MainRenderImage, - DarkStatusImage, LightStatusImage, ]: entities.append( @@ -137,18 +136,3 @@ class LightStatusImage(StatusImage): def image_url(self) -> str | None: if status := self.vehicle.status: return status.renders.light_mode.three_x - - -class DarkStatusImage(StatusImage): - """Dark 3x render of the vehicle status.""" - - entity_description = ImageEntityDescription( - key="render_dark_3x", - translation_key="render_dark_3x", - entity_registry_enabled_default=False, - ) - - @property - def image_url(self) -> str | None: - if status := self.vehicle.status: - return status.renders.dark_mode.three_x From 0e1909dba1c6655b5dbd2390fbff706f62938dd9 Mon Sep 17 00:00:00 2001 From: Nils Vogels Date: Wed, 11 Dec 2024 23:11:48 +0100 Subject: [PATCH 4/7] Remove translation of dark render as well --- custom_components/myskoda/translations/en.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/custom_components/myskoda/translations/en.json b/custom_components/myskoda/translations/en.json index 78c7fe9..2cc43c2 100644 --- a/custom_components/myskoda/translations/en.json +++ b/custom_components/myskoda/translations/en.json @@ -109,9 +109,6 @@ } }, "image": { - "render_dark_3x": { - "name": "Dark Status Render of Vehicle" - }, "render_light_3x": { "name": "Light Status Render of Vehicle" }, From abe7525ac51fdb6d5f898e451319ca5a61ccb9e4 Mon Sep 17 00:00:00 2001 From: Nils Vogels Date: Thu, 12 Dec 2024 13:47:59 +0100 Subject: [PATCH 5/7] Allow the status entity to update itself --- custom_components/myskoda/image.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/myskoda/image.py b/custom_components/myskoda/image.py index aac9d99..5811420 100644 --- a/custom_components/myskoda/image.py +++ b/custom_components/myskoda/image.py @@ -67,6 +67,8 @@ def __init__( class StatusImage(MySkodaImage): """A render of the current status of the vehicle.""" + _attr_should_poll: bool = True + async def _fetch_url(self, url: str) -> httpx.Response | None: """Fetch a URL passing in the MySkoda access token.""" From 501807d284255a206f3aae397051f4deb4a24f25 Mon Sep 17 00:00:00 2001 From: Nils Vogels Date: Mon, 16 Dec 2024 10:30:01 +0100 Subject: [PATCH 6/7] Implement last changed tracker --- custom_components/myskoda/image.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/custom_components/myskoda/image.py b/custom_components/myskoda/image.py index 5811420..28f5d18 100644 --- a/custom_components/myskoda/image.py +++ b/custom_components/myskoda/image.py @@ -1,5 +1,6 @@ """Images for the MySkoda integration.""" +from datetime import datetime as dt import httpx import logging @@ -138,3 +139,8 @@ class LightStatusImage(StatusImage): def image_url(self) -> str | None: if status := self.vehicle.status: return status.renders.light_mode.three_x + + @property + def image_last_updated(self) -> dt | None: + if status := self.vehicle.status: + return status.car_captured_timestamp From c970710faf0ae9c24d45ff85f0a61efb746d5cf4 Mon Sep 17 00:00:00 2001 From: Nils Vogels Date: Thu, 19 Dec 2024 10:55:13 +0100 Subject: [PATCH 7/7] Force cache invalidation when timestamp changes --- custom_components/myskoda/image.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/custom_components/myskoda/image.py b/custom_components/myskoda/image.py index 28f5d18..8bbf3ce 100644 --- a/custom_components/myskoda/image.py +++ b/custom_components/myskoda/image.py @@ -1,6 +1,5 @@ """Images for the MySkoda integration.""" -from datetime import datetime as dt import httpx import logging @@ -13,7 +12,7 @@ from homeassistant.const import ( EntityCategory, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import DiscoveryInfoType # pyright: ignore [reportAttributeAccessIssue] @@ -68,8 +67,6 @@ def __init__( class StatusImage(MySkodaImage): """A render of the current status of the vehicle.""" - _attr_should_poll: bool = True - async def _fetch_url(self, url: str) -> httpx.Response | None: """Fetch a URL passing in the MySkoda access token.""" @@ -96,6 +93,17 @@ async def _fetch_url(self, url: str) -> httpx.Response | None: return None return response + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if status := self.vehicle.status: + if status.car_captured_timestamp != self._attr_image_last_updated: + _LOGGER.debug("Image updated. Flushing caches.") + + self._cached_image = None + self._attr_image_last_updated = status.car_captured_timestamp + super()._handle_coordinator_update() + class MainRenderImage(MySkodaImage): """Main render of the vehicle.""" @@ -139,8 +147,3 @@ class LightStatusImage(StatusImage): def image_url(self) -> str | None: if status := self.vehicle.status: return status.renders.light_mode.three_x - - @property - def image_last_updated(self) -> dt | None: - if status := self.vehicle.status: - return status.car_captured_timestamp