Skip to content

Commit

Permalink
Merge pull request #526 from NickM-27/unavailable
Browse files Browse the repository at this point in the history
Set camera to unavailable when camera fps == 0
  • Loading branch information
dermotduffy authored Aug 26, 2023
2 parents 44482fe + 701daaf commit d78b803
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 4 deletions.
4 changes: 3 additions & 1 deletion custom_components/frigate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ def __init__(self, config_entry: ConfigEntry):
@property
def available(self) -> bool:
"""Return the availability of the entity."""
return self._available
return self._available and super().available

def _get_model(self) -> str:
"""Get the Frigate device model string."""
Expand Down Expand Up @@ -451,11 +451,13 @@ async def async_added_to_hass(self) -> None:
self._topic_map,
)
self._sub_state = await async_subscribe_topics(self.hass, state)
await super().async_added_to_hass()

async def async_will_remove_from_hass(self) -> None:
"""Cleanup prior to hass removal."""
async_unsubscribe_topics(self.hass, self._sub_state)
self._sub_state = None
await super().async_will_remove_from_hass()

@callback # type: ignore[misc]
def _availability_message_received(self, msg: ReceiveMessage) -> None:
Expand Down
17 changes: 16 additions & 1 deletion custom_components/frigate/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from . import (
FrigateDataUpdateCoordinator,
FrigateEntity,
FrigateMQTTEntity,
ReceiveMessage,
Expand All @@ -33,6 +35,7 @@
from .const import (
ATTR_CLIENT,
ATTR_CONFIG,
ATTR_COORDINATOR,
ATTR_EVENT_ID,
ATTR_FAVORITE,
ATTR_PTZ_ACTION,
Expand Down Expand Up @@ -60,6 +63,7 @@ async def async_setup_entry(
frigate_config = hass.data[DOMAIN][entry.entry_id][ATTR_CONFIG]
frigate_client = hass.data[DOMAIN][entry.entry_id][ATTR_CLIENT]
client_id = get_frigate_instance_id_for_config_entry(hass, entry)
coordinator = hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATOR]

async_add_entities(
[
Expand All @@ -68,6 +72,7 @@ async def async_setup_entry(
cam_name,
frigate_client,
client_id,
coordinator,
frigate_config,
camera_config,
)
Expand Down Expand Up @@ -104,7 +109,7 @@ async def async_setup_entry(
)


class FrigateCamera(FrigateMQTTEntity, Camera): # type: ignore[misc]
class FrigateCamera(FrigateMQTTEntity, CoordinatorEntity, Camera): # type: ignore[misc]
"""Representation of a Frigate camera."""

# sets the entity name to same as device name ex: camera.front_doorbell
Expand All @@ -116,6 +121,7 @@ def __init__(
cam_name: str,
frigate_client: FrigateApiClient,
frigate_client_id: Any | None,
coordinator: FrigateDataUpdateCoordinator,
frigate_config: dict[str, Any],
camera_config: dict[str, Any],
) -> None:
Expand Down Expand Up @@ -150,6 +156,7 @@ def __init__(
},
)
FrigateEntity.__init__(self, config_entry)
CoordinatorEntity.__init__(self, coordinator)
Camera.__init__(self)
self._url = config_entry.data[CONF_URL]
self._attr_is_on = True
Expand Down Expand Up @@ -227,6 +234,14 @@ def _motion_message_received(self, msg: ReceiveMessage) -> None:
self._attr_motion_detection_enabled = msg.payload.decode("utf-8") == "ON"
self.async_write_ha_state()

@property
def available(self) -> bool:
"""Signal when frigate loses connection to camera."""
if self.coordinator.data:
if self.coordinator.data.get(self._cam_name, {}).get("camera_fps", 0) == 0:
return False
return super().available

@property
def unique_id(self) -> str:
"""Return a unique ID to use for this entity."""
Expand Down
29 changes: 28 additions & 1 deletion tests/test_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
from unittest.mock import AsyncMock

import pytest
from pytest_homeassistant_custom_component.common import async_fire_mqtt_message
from pytest_homeassistant_custom_component.common import (
async_fire_mqtt_message,
async_fire_time_changed,
)

from custom_components.frigate import SCAN_INTERVAL
from custom_components.frigate.const import (
ATTR_EVENT_ID,
ATTR_FAVORITE,
Expand All @@ -31,6 +35,7 @@
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
import homeassistant.util.dt as dt_util

from . import (
TEST_CAMERA_BIRDSEYE_ENTITY_ID,
Expand All @@ -40,6 +45,7 @@
TEST_CONFIG_ENTRY_ID,
TEST_FRIGATE_INSTANCE_ID,
TEST_SERVER_VERSION,
TEST_STATS,
create_mock_frigate_client,
create_mock_frigate_config_entry,
setup_mock_frigate_config_entry,
Expand Down Expand Up @@ -355,6 +361,27 @@ async def test_camera_disable_motion_detection(
)


async def test_camera_unavailable(hass: HomeAssistant) -> None:
"""Test that camera is marked as unavailable."""
client = create_mock_frigate_client()
stats: dict[str, Any] = copy.deepcopy(TEST_STATS)
client.async_get_stats = AsyncMock(return_value=stats)
await setup_mock_frigate_config_entry(hass, client=client)

entity_state = hass.states.get(TEST_CAMERA_FRONT_DOOR_ENTITY_ID)
assert entity_state
assert entity_state.state == "streaming"

stats["front_door"]["camera_fps"] = 0.0

async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()

entity_state = hass.states.get(TEST_CAMERA_FRONT_DOOR_ENTITY_ID)
assert entity_state
assert entity_state.state == "unavailable"


@pytest.mark.parametrize(
"entityid_to_uniqueid",
[
Expand Down
7 changes: 6 additions & 1 deletion tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,13 +284,18 @@ async def test_status_sensor_error(hass: HomeAssistant) -> None:
await setup_mock_frigate_config_entry(hass, client=client)
await enable_and_load_entity(hass, client, TEST_SENSOR_FRIGATE_STATUS_ENTITY_ID)

async_fire_mqtt_message(hass, "frigate/available", "online")
await hass.async_block_till_done()

client.async_get_stats = AsyncMock(side_effect=FrigateApiClientError)
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()

entity_state = hass.states.get(TEST_SENSOR_FRIGATE_STATUS_ENTITY_ID)
assert entity_state
assert entity_state.state == "error"

# The update coordinator will treat the error as unavailability.
assert entity_state.state == "unavailable"
assert entity_state.attributes["icon"] == ICON_SERVER


Expand Down

0 comments on commit d78b803

Please sign in to comment.