From 18022f73116e29a30c49d17f82f280474aaae486 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 21 Apr 2022 23:10:09 -0600 Subject: [PATCH] Add an 'update' entity to report new versions of the Frigate backend (#237) * Add update entity * Add update tests * Add update to supported entities * Update testing values * Fixing tests * Fix tests * Use property not attr * use attributes * use attributes * Adjust correct test * Adjust correct test * Set update entity category as system not diagnostic * Revert * Update name to fit all use cases * Make unique_id not break redundancy * Via device * Add constant for tag release url * Inherit from frigate entity * Cleanup tests * Cleanup tests * Update to name attr * Remove unnecessary log error * Update test update entity name * remove name property * Fix inheritence * Use better string templating * Use better string templating * Update unique id * Improve string parsing * Update with bad data is unknown --- custom_components/frigate/const.py | 4 +- custom_components/frigate/update.py | 100 ++++++++++++++++++++++++++ tests/__init__.py | 2 + tests/test_update.py | 108 ++++++++++++++++++++++++++++ 4 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 custom_components/frigate/update.py create mode 100644 tests/test_update.py diff --git a/custom_components/frigate/const.py b/custom_components/frigate/const.py index ff8f3220..fc5fb277 100644 --- a/custom_components/frigate/const.py +++ b/custom_components/frigate/const.py @@ -4,6 +4,7 @@ DOMAIN = "frigate" FRIGATE_VERSION_ERROR_CUTOFF = "0.8.4" FRIGATE_RELEASES_URL = "https://github.com/blakeblackshear/frigate/releases" +FRIGATE_RELEASE_TAG_URL = f"{FRIGATE_RELEASES_URL}/tag" # Icons ICON_CAR = "mdi:shield-car" @@ -22,7 +23,8 @@ SENSOR = "sensor" SWITCH = "switch" CAMERA = "camera" -PLATFORMS = [SENSOR, CAMERA, SWITCH, BINARY_SENSOR] +UPDATE = "update" +PLATFORMS = [SENSOR, CAMERA, SWITCH, BINARY_SENSOR, UPDATE] # Unit of measurement FPS = "fps" diff --git a/custom_components/frigate/update.py b/custom_components/frigate/update.py new file mode 100644 index 00000000..7b1f4a1a --- /dev/null +++ b/custom_components/frigate/update.py @@ -0,0 +1,100 @@ +"""Update platform for frigate.""" +from __future__ import annotations + +import logging + +from homeassistant.components.update import UpdateEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import ( + FrigateDataUpdateCoordinator, + FrigateEntity, + get_frigate_device_identifier, + get_frigate_entity_unique_id, +) +from .const import ATTR_COORDINATOR, DOMAIN, FRIGATE_RELEASE_TAG_URL, NAME + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Sensor entry setup.""" + coordinator = hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATOR] + + entities = [] + entities.append(FrigateContainerUpdate(coordinator, entry)) + async_add_entities(entities) + + +class FrigateContainerUpdate(FrigateEntity, UpdateEntity, CoordinatorEntity): # type: ignore[misc] + """Frigate container update.""" + + _attr_name = "Frigate Server" + + def __init__( + self, + coordinator: FrigateDataUpdateCoordinator, + config_entry: ConfigEntry, + ) -> None: + """Construct a FrigateContainerUpdate.""" + FrigateEntity.__init__(self, config_entry) + CoordinatorEntity.__init__(self, coordinator) + + @property + def unique_id(self) -> str: + """Return a unique ID to use for this entity.""" + return get_frigate_entity_unique_id( + self._config_entry.entry_id, "update", "frigate_server" + ) + + @property + def device_info(self) -> DeviceInfo: + """Get device information.""" + return { + "identifiers": {get_frigate_device_identifier(self._config_entry)}, + "via_device": get_frigate_device_identifier(self._config_entry), + "name": NAME, + "model": self._get_model(), + "configuration_url": self._config_entry.data.get(CONF_URL), + "manufacturer": NAME, + } + + @property + def installed_version(self) -> str | None: + """Version currently in use.""" + + version_hash = self.coordinator.data.get("service", {}).get("version") + + if not version_hash: + return None + + version = str(version_hash).split("-")[0] + + return version + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + + version = self.coordinator.data.get("service", {}).get("latest_version") + + if not version or version == "unknown": + return None + + return str(version) + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + + if (version := self.latest_version) is None: + return None + + return f"{FRIGATE_RELEASE_TAG_URL}/v{version}" diff --git a/tests/__init__.py b/tests/__init__.py index 7f6fdf69..5a34c16d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -33,6 +33,7 @@ TEST_SENSOR_FRONT_DOOR_DETECTION_FPS_ENTITY_ID = "sensor.front_door_detection_fps" TEST_SENSOR_FRONT_DOOR_PROCESS_FPS_ENTITY_ID = "sensor.front_door_process_fps" TEST_SENSOR_FRONT_DOOR_SKIPPED_FPS_ENTITY_ID = "sensor.front_door_skipped_fps" +TEST_UPDATE_FRIGATE_CONTAINER_ENTITY_ID = "update.frigate_server" TEST_SERVER_VERSION = "0.9.0-09a4d6d" TEST_CONFIG_ENTRY_ID = "74565ad414754616000674c87bdc876c" @@ -170,6 +171,7 @@ }, "uptime": 101113, "version": "0.8.4-09a4d6d", + "latest_version": "0.10.1", }, } TEST_EVENT_SUMMARY = [ diff --git a/tests/test_update.py b/tests/test_update.py new file mode 100644 index 00000000..44e83efe --- /dev/null +++ b/tests/test_update.py @@ -0,0 +1,108 @@ +"""Test the frigate updaters.""" +from __future__ import annotations + +import copy +import logging +from typing import Any +from unittest.mock import AsyncMock + +from pytest_homeassistant_custom_component.common import async_fire_time_changed + +from custom_components.frigate import SCAN_INTERVAL +from custom_components.frigate.const import FRIGATE_RELEASE_TAG_URL +from homeassistant.components.update.const import ( + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, + ATTR_RELEASE_URL, +) +from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util + +from . import ( + TEST_STATS, + TEST_UPDATE_FRIGATE_CONTAINER_ENTITY_ID, + create_mock_frigate_client, + setup_mock_frigate_config_entry, +) + +_LOGGER = logging.getLogger(__name__) + + +async def test_update_sensor_new_update(hass: HomeAssistant) -> None: + """Test FrigateUpdateSensor state.""" + + client = create_mock_frigate_client() + await setup_mock_frigate_config_entry(hass, client=client) + + entity_state = hass.states.get(TEST_UPDATE_FRIGATE_CONTAINER_ENTITY_ID) + assert entity_state + assert entity_state.state == "on" + assert ( + entity_state.attributes[ATTR_RELEASE_URL] + == f"{FRIGATE_RELEASE_TAG_URL}/v0.10.1" + ) + + +async def test_update_sensor_same_version(hass: HomeAssistant) -> None: + """Test FrigateUpdateSensor state.""" + + client = create_mock_frigate_client() + await setup_mock_frigate_config_entry(hass, client=client) + + stats: dict[str, Any] = copy.deepcopy(TEST_STATS) + client.async_get_stats = AsyncMock(return_value=stats) + + stats["service"]["version"] = stats["service"]["latest_version"] + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + entity_state = hass.states.get(TEST_UPDATE_FRIGATE_CONTAINER_ENTITY_ID) + assert entity_state + assert entity_state.state == "off" + assert entity_state.attributes[ATTR_INSTALLED_VERSION] == "0.10.1" + assert entity_state.attributes[ATTR_LATEST_VERSION] == "0.10.1" + assert ( + entity_state.attributes[ATTR_RELEASE_URL] + == f"{FRIGATE_RELEASE_TAG_URL}/v0.10.1" + ) + + +async def test_update_sensor_bad_current(hass: HomeAssistant) -> None: + """Test FrigateUpdateSensor state.""" + + client = create_mock_frigate_client() + await setup_mock_frigate_config_entry(hass, client=client) + + stats: dict[str, Any] = copy.deepcopy(TEST_STATS) + client.async_get_stats = AsyncMock(return_value=stats) + + stats["service"]["version"] = "" + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + entity_state = hass.states.get(TEST_UPDATE_FRIGATE_CONTAINER_ENTITY_ID) + assert entity_state + assert entity_state.state == "unknown" + assert entity_state.attributes[ATTR_INSTALLED_VERSION] is None + assert entity_state.attributes[ATTR_LATEST_VERSION] == "0.10.1" + + +async def test_update_sensor_bad_latest(hass: HomeAssistant) -> None: + """Test FrigateUpdateSensor state.""" + + client = create_mock_frigate_client() + await setup_mock_frigate_config_entry(hass, client=client) + + stats: dict[str, Any] = copy.deepcopy(TEST_STATS) + client.async_get_stats = AsyncMock(return_value=stats) + + stats["service"]["latest_version"] = "unknown" + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + entity_state = hass.states.get(TEST_UPDATE_FRIGATE_CONTAINER_ENTITY_ID) + assert entity_state + assert entity_state.state == "unknown" + assert entity_state.attributes[ATTR_INSTALLED_VERSION] == "0.8.4" + assert entity_state.attributes[ATTR_LATEST_VERSION] is None + assert entity_state.attributes[ATTR_RELEASE_URL] is None