diff --git a/custom_components/frigate/const.py b/custom_components/frigate/const.py index 9204d452..dd3a667c 100644 --- a/custom_components/frigate/const.py +++ b/custom_components/frigate/const.py @@ -25,6 +25,7 @@ # Unit of measurement FPS = "fps" MS = "ms" +S = "s" # Attributes ATTR_CLIENT = "client" diff --git a/custom_components/frigate/icons.py b/custom_components/frigate/icons.py index e8a12807..c62e1e1f 100644 --- a/custom_components/frigate/icons.py +++ b/custom_components/frigate/icons.py @@ -20,6 +20,7 @@ ICON_SERVER = "mdi:server" ICON_SPEEDOMETER = "mdi:speedometer" ICON_WAVEFORM = "mdi:waveform" +ICON_UPTIME = "mdi:clock-time-five" ICON_DEFAULT_ON = "mdi:home" diff --git a/custom_components/frigate/sensor.py b/custom_components/frigate/sensor.py index 8049c3c3..85f8725a 100644 --- a/custom_components/frigate/sensor.py +++ b/custom_components/frigate/sensor.py @@ -29,11 +29,12 @@ get_frigate_entity_unique_id, get_zones, ) -from .const import ATTR_CONFIG, ATTR_COORDINATOR, DOMAIN, FPS, MS, NAME +from .const import ATTR_CONFIG, ATTR_COORDINATOR, DOMAIN, FPS, MS, NAME, S from .icons import ( ICON_CORAL, ICON_SERVER, ICON_SPEEDOMETER, + ICON_UPTIME, ICON_WAVEFORM, get_icon_from_type, ) @@ -104,6 +105,7 @@ async def async_setup_entry( ] ) entities.append(FrigateStatusSensor(coordinator, entry)) + entities.append(FrigateUptimeSensor(coordinator, entry)) async_add_entities(entities) @@ -207,6 +209,62 @@ def icon(self) -> str: return ICON_SERVER +class FrigateUptimeSensor( + FrigateEntity, CoordinatorEntity[FrigateDataUpdateCoordinator] +): + """Frigate Uptime Sensor class.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_name = "Uptime" + + def __init__( + self, coordinator: FrigateDataUpdateCoordinator, config_entry: ConfigEntry + ) -> None: + """Construct a FrigateUptimeSensor.""" + FrigateEntity.__init__(self, config_entry) + CoordinatorEntity.__init__(self, coordinator) + self._attr_entity_registry_enabled_default = False + + @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, "uptime", "frigate" + ) + + @property + def device_info(self) -> DeviceInfo: + """Get device information.""" + return { + "identifiers": {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 state(self) -> int | None: + """Return the state of the sensor.""" + if self.coordinator.data: + data = self.coordinator.data.get("service", {}).get("uptime", 0) + try: + return int(data) + except (TypeError, ValueError): + pass + return None + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of the sensor.""" + return S + + @property + def icon(self) -> str: + """Return the icon of the sensor.""" + return ICON_UPTIME + + class DetectorSpeedSensor( FrigateEntity, CoordinatorEntity[FrigateDataUpdateCoordinator] ): diff --git a/tests/__init__.py b/tests/__init__.py index 0fa10b5c..1923a334 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -69,6 +69,7 @@ TEST_SENSOR_FRONT_DOOR_SKIPPED_FPS_ENTITY_ID = "sensor.front_door_skipped_fps" TEST_SENSOR_FRONT_DOOR_SOUND_LEVEL_ID = "sensor.front_door_sound_level" TEST_SENSOR_FRIGATE_STATUS_ENTITY_ID = "sensor.frigate_status" +TEST_SENSOR_FRIGATE_UPTIME_ENTITY_ID = "sensor.frigate_uptime" TEST_UPDATE_FRIGATE_CONTAINER_ENTITY_ID = "update.frigate_server" TEST_SERVER_VERSION = "0.14.1-f4f3cfa" diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 9771fd35..fbcae876 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -29,6 +29,7 @@ ICON_PERSON, ICON_SERVER, ICON_SPEEDOMETER, + ICON_UPTIME, ICON_WAVEFORM, ) from homeassistant.const import PERCENTAGE, UnitOfSoundPressure, UnitOfTemperature @@ -44,6 +45,7 @@ TEST_SENSOR_CPU2_INTFERENCE_SPEED_ENTITY_ID, TEST_SENSOR_DETECTION_FPS_ENTITY_ID, TEST_SENSOR_FRIGATE_STATUS_ENTITY_ID, + TEST_SENSOR_FRIGATE_UPTIME_ENTITY_ID, TEST_SENSOR_FRONT_DOOR_ALL_ACTIVE_ENTITY_ID, TEST_SENSOR_FRONT_DOOR_ALL_ENTITY_ID, TEST_SENSOR_FRONT_DOOR_CAMERA_FPS_ENTITY_ID, @@ -375,6 +377,38 @@ async def test_status_sensor_error(hass: HomeAssistant) -> None: assert entity_state.attributes["icon"] == ICON_SERVER +async def test_uptime_sensor(hass: HomeAssistant) -> None: + """Test FrigateUptimeSensor expected state.""" + + client = create_mock_frigate_client() + await setup_mock_frigate_config_entry(hass, client=client) + await enable_and_load_entity(hass, client, TEST_SENSOR_FRIGATE_UPTIME_ENTITY_ID) + + entity_state = hass.states.get(TEST_SENSOR_FRIGATE_UPTIME_ENTITY_ID) + assert entity_state + assert entity_state.state == "101113" + assert entity_state.attributes["icon"] == ICON_UPTIME + + stats: dict[str, Any] = copy.deepcopy(TEST_STATS) + client.async_get_stats = AsyncMock(return_value=stats) + + stats["service"]["uptime"] = None + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + entity_state = hass.states.get(TEST_SENSOR_FRIGATE_UPTIME_ENTITY_ID) + assert entity_state + assert entity_state.state == "unknown" + + stats["service"]["uptime"] = "NOT_A_NUMBER" + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + entity_state = hass.states.get(TEST_SENSOR_FRIGATE_UPTIME_ENTITY_ID) + assert entity_state + assert entity_state.state == "unknown" + + async def test_per_entry_device_info(hass: HomeAssistant) -> None: """Verify switch device information.""" config_entry = await setup_mock_frigate_config_entry(hass)