Skip to content

Commit

Permalink
Add an 'update' entity to report new versions of the Frigate backend (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
NickM-27 authored Apr 22, 2022
1 parent 65e5ec0 commit 18022f7
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 1 deletion.
4 changes: 3 additions & 1 deletion custom_components/frigate/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
100 changes: 100 additions & 0 deletions custom_components/frigate/update.py
Original file line number Diff line number Diff line change
@@ -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}"
2 changes: 2 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -170,6 +171,7 @@
},
"uptime": 101113,
"version": "0.8.4-09a4d6d",
"latest_version": "0.10.1",
},
}
TEST_EVENT_SUMMARY = [
Expand Down
108 changes: 108 additions & 0 deletions tests/test_update.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 18022f7

Please sign in to comment.