Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set camera to unavailable when camera fps == 0 #526

Merged
merged 5 commits into from
Aug 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading