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

Update entities to respect new enable/visibility principles #273

Merged
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
6 changes: 4 additions & 2 deletions custom_components/frigate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
f"{cam_name}_{obj_name}",
)
entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, unique_id)
if entity_id:
new_id = f"sensor.{cam_name}_{obj_name}_count"
new_id = f"sensor.{cam_name}_{obj_name}_count"
# Verify the new entity_id doesn't already exist.
entry_for_new_id = entity_registry.async_get(new_id)
if entity_id and entity_id != new_id and not entry_for_new_id:
new_name = f"{get_friendly_name(cam_name)} {obj_name} Count".title()
entity_registry.async_update_entity(
entity_id=entity_id,
Expand Down
10 changes: 0 additions & 10 deletions custom_components/frigate/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,6 @@ def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self._is_on

@property
def entity_registry_enabled_default(self) -> bool:
"""Whether or not the entity is enabled by default."""
return self._obj_name != "all"

@property
def device_class(self) -> str:
"""Return the device class."""
Expand Down Expand Up @@ -200,11 +195,6 @@ def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self._is_on

@property
def entity_registry_enabled_default(self) -> bool:
"""Whether or not the entity is enabled by default."""
return False

@property
def device_class(self) -> str:
"""Return the device class."""
Expand Down
8 changes: 3 additions & 5 deletions custom_components/frigate/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def __init__(
"""Construct a FrigateFpsSensor."""
FrigateEntity.__init__(self, config_entry)
CoordinatorEntity.__init__(self, coordinator)
self._attr_entity_registry_enabled_default = False

@property
def unique_id(self) -> str:
Expand Down Expand Up @@ -147,6 +148,7 @@ def __init__(
FrigateEntity.__init__(self, config_entry)
CoordinatorEntity.__init__(self, coordinator)
self._detector_name = detector_name
self._attr_entity_registry_enabled_default = False

@property
def unique_id(self) -> str:
Expand Down Expand Up @@ -215,6 +217,7 @@ def __init__(
CoordinatorEntity.__init__(self, coordinator)
self._cam_name = cam_name
self._fps_type = fps_type
self._attr_entity_registry_enabled_default = False

@property
def unique_id(self) -> str:
Expand Down Expand Up @@ -360,8 +363,3 @@ def unit_of_measurement(self) -> str:
def icon(self) -> str:
"""Return the icon of the sensor."""
return self._icon

@property
def entity_registry_enabled_default(self) -> bool:
"""Whether or not the entity is enabled by default."""
return self._obj_name != "all"
2 changes: 1 addition & 1 deletion custom_components/frigate/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async def async_setup_entry(
entities.extend(
[
FrigateSwitch(entry, frigate_config, camera, "detect", True),
FrigateSwitch(entry, frigate_config, camera, "motion", False),
FrigateSwitch(entry, frigate_config, camera, "motion", True),
FrigateSwitch(entry, frigate_config, camera, "recordings", True),
FrigateSwitch(entry, frigate_config, camera, "snapshots", True),
FrigateSwitch(entry, frigate_config, camera, "improve_contrast", False),
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
aiohttp
aiohttp_cors==0.7.0
attr
homeassistant>=2022.4.5
homeassistant>=2022.6.1
dermotduffy marked this conversation as resolved.
Show resolved Hide resolved
paho-mqtt==1.6.1
python-dateutil
yarl
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ flake8
mypy==0.910
pre-commit
pytest==7.1.1
pytest-homeassistant-custom-component>=0.8.10
pytest-homeassistant-custom-component>=0.9.11
pylint-pytest
pylint==2.8.3
pytest-aiohttp==0.3.0
Expand Down
88 changes: 85 additions & 3 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
"""Tests for the Frigate integration."""
from __future__ import annotations

from datetime import timedelta
from typing import Any
from unittest.mock import AsyncMock, patch

from aiohttp import web
from pytest_homeassistant_custom_component.common import MockConfigEntry
from pytest_homeassistant_custom_component.common import (
MockConfigEntry,
async_fire_time_changed,
)

from custom_components.frigate.const import DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntry
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.util.dt as dt_util

TEST_BINARY_SENSOR_FRONT_DOOR_MOTION_ENTITY_ID = "binary_sensor.front_door_motion"
TEST_BINARY_SENSOR_FRONT_DOOR_PERSON_OCCUPANCY_ENTITY_ID = (
"binary_sensor.front_door_person_occupancy"
)
TEST_BINARY_SENSOR_FRONT_DOOR_ALL_OCCUPANCY_ENTITY_ID = (
"binary_sensor.front_door_all_occupancy"
)
TEST_BINARY_SENSOR_STEPS_PERSON_OCCUPANCY_ENTITY_ID = (
"binary_sensor.steps_person_occupancy"
)
TEST_BINARY_SENSOR_STEPS_ALL_OCCUPANCY_ENTITY_ID = "binary_sensor.steps_all_occupancy"
TEST_CAMERA_FRONT_DOOR_ENTITY_ID = "camera.front_door"
TEST_CAMERA_FRONT_DOOR_PERSON_ENTITY_ID = "camera.front_door_person"

TEST_SWITCH_FRONT_DOOR_DETECT_ENTITY_ID = "switch.front_door_detect"
TEST_SWITCH_FRONT_DOOR_MOTION_ENTITY_ID = "switch.front_door_motion"
TEST_SWITCH_FRONT_DOOR_SNAPSHOTS_ENTITY_ID = "switch.front_door_snapshots"
TEST_SWITCH_FRONT_DOOR_RECORDINGS_ENTITY_ID = "switch.front_door_recordings"
TEST_SWITCH_FRONT_DOOR_IMPROVE_CONTRAST_ENTITY_ID = "switch.front_door_improve_contrast"
TEST_SENSOR_STEPS_PERSON_ENTITY_ID = "sensor.steps_person_count"

TEST_SENSOR_STEPS_ALL_ENTITY_ID = "sensor.steps_all_count"
TEST_SENSOR_STEPS_PERSON_ENTITY_ID = "sensor.steps_person_count"
TEST_SENSOR_FRONT_DOOR_ALL_ENTITY_ID = "sensor.front_door_all_count"
TEST_SENSOR_FRONT_DOOR_PERSON_ENTITY_ID = "sensor.front_door_person_count"
TEST_SENSOR_DETECTION_FPS_ENTITY_ID = "sensor.detection_fps"
TEST_SENSOR_CPU1_INTFERENCE_SPEED_ENTITY_ID = "sensor.cpu1_inference_speed"
Expand Down Expand Up @@ -307,3 +320,72 @@ async def setup_mock_frigate_config_entry(
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry


async def test_entities_are_setup_correctly_in_registry(
hass: HomeAssistant,
entities_enabled: set[str] | None = None,
entities_disabled: set[str] | None = None,
entities_visible: set[str] | None = None,
entities_hidden: set[str] | None = None,
) -> None:
"""Verify entities are enabled/visible as appropriate."""

registry = er.async_get(hass)

for entity in entities_enabled or {}:
entry = registry.async_get(entity)
assert entry
assert not entry.disabled
assert not entry.disabled_by

for entity in entities_disabled or {}:
entry = registry.async_get(entity)
assert entry
assert entry.disabled
assert entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION

entity_state = hass.states.get(entity)
assert not entity_state

# Update and test that entity is now enabled.
updated_entry = registry.async_update_entity(entity, disabled_by=None)
assert not updated_entry.disabled
assert not updated_entry.disabled_by

for entity in entities_visible or {}:
entry = registry.async_get(entity)
assert entry
assert not entry.hidden
assert not entry.hidden_by

for entity in entities_hidden or {}:
entry = registry.async_get(entity)
assert entry
assert entry.hidden
assert entry.hidden_by == er.RegistryEntryHider.INTEGRATION

# Update and test that entity is now visible.
updated_entry = registry.async_update_entity(entity, hidden_by=None)
assert not updated_entry.hidden
assert not updated_entry.hidden_by


async def enable_and_load_entity(
hass: HomeAssistant, client: AsyncMock, entity: str
) -> None:
"""Enable and load an entity."""

# Keep the patch in place to ensure that coordinator updates that are
# scheduled during the reload period will use the mocked API.
with patch(
"custom_components.frigate.FrigateApiClient",
return_value=client,
):
er.async_get(hass).async_update_entity(entity, disabled_by=None)
await hass.async_block_till_done()
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
await hass.async_block_till_done()
96 changes: 40 additions & 56 deletions tests/test_binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
"""Test the frigate binary sensor."""
from __future__ import annotations

from datetime import timedelta
import logging
from typing import Any
from unittest.mock import AsyncMock, Mock, patch

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

from custom_components.frigate.api import FrigateApiClientError
from custom_components.frigate.const import DOMAIN, NAME
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
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_BINARY_SENSOR_FRONT_DOOR_ALL_OCCUPANCY_ENTITY_ID,
TEST_BINARY_SENSOR_FRONT_DOOR_MOTION_ENTITY_ID,
TEST_BINARY_SENSOR_FRONT_DOOR_PERSON_OCCUPANCY_ENTITY_ID,
TEST_BINARY_SENSOR_STEPS_ALL_OCCUPANCY_ENTITY_ID,
Expand All @@ -28,6 +23,7 @@
TEST_SERVER_VERSION,
create_mock_frigate_client,
setup_mock_frigate_config_entry,
test_entities_are_setup_correctly_in_registry,
)

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -82,43 +78,25 @@ async def test_occupancy_binary_sensor_setup(hass: HomeAssistant) -> None:
assert entity_state.state == "unavailable"


async def test_binary_sensor_motion_can_be_enabled(hass: HomeAssistant) -> None:
"""Verify motion binary_sensor can be enabled and used."""
client = create_mock_frigate_client()
await setup_mock_frigate_config_entry(hass, client=client)

entity_registry = er.async_get(hass)
expected_results = {
TEST_BINARY_SENSOR_FRONT_DOOR_MOTION_ENTITY_ID: "frigate/front_door/motion",
}

# Keep the patch in place to ensure that coordinator updates that are
# scheduled during the reload period will use the mocked API.
with patch(
"custom_components.frigate.FrigateApiClient",
return_value=client,
):
for disabled_entity_id, mqtt_topic in expected_results.items():
updated_entry = entity_registry.async_update_entity(
disabled_entity_id, disabled_by=None
)
assert not updated_entry.disabled
await hass.async_block_till_done()

async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
await hass.async_block_till_done()
async def test_motion_binary_sensor_setup(hass: HomeAssistant) -> None:
"""Verify a successful motion binary sensor setup."""
await setup_mock_frigate_config_entry(hass)

async_fire_mqtt_message(hass, "frigate/available", "online")
await hass.async_block_till_done()
entity_state = hass.states.get(TEST_BINARY_SENSOR_FRONT_DOOR_MOTION_ENTITY_ID)
assert entity_state
assert entity_state.state == "unavailable"

async_fire_mqtt_message(hass, mqtt_topic, "ON")
await hass.async_block_till_done()
async_fire_mqtt_message(hass, "frigate/available", "online")
await hass.async_block_till_done()
entity_state = hass.states.get(TEST_BINARY_SENSOR_FRONT_DOOR_MOTION_ENTITY_ID)
assert entity_state
assert entity_state.state == "off"

entity_state = hass.states.get(disabled_entity_id)
assert entity_state.state == "on"
async_fire_mqtt_message(hass, "frigate/front_door/motion", "ON")
await hass.async_block_till_done()
entity_state = hass.states.get(TEST_BINARY_SENSOR_FRONT_DOOR_MOTION_ENTITY_ID)
assert entity_state
assert entity_state.state == "on"


async def test_binary_sensor_api_call_failed(hass: HomeAssistant) -> None:
Expand Down Expand Up @@ -185,21 +163,27 @@ async def test_binary_sensor_unload_will_unsubscribe(hass: HomeAssistant) -> Non
mock_unsubscribe.assert_called()


async def test_binary_sensor_all_can_be_enabled(hass: HomeAssistant) -> None:
"""Verify `all` binary_sensor can be enabled."""
async def test_binary_sensors_setup_correctly_in_registry(
aiohttp_server: Any, hass: HomeAssistant
) -> None:
"""Verify entities are enabled/visible as appropriate."""

await setup_mock_frigate_config_entry(hass)
entity_registry = er.async_get(hass)

# Test original entity is disabled as expected
entry = entity_registry.async_get(TEST_BINARY_SENSOR_STEPS_ALL_OCCUPANCY_ENTITY_ID)
assert entry
assert entry.disabled
assert entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION
entity_state = hass.states.get(TEST_BINARY_SENSOR_STEPS_ALL_OCCUPANCY_ENTITY_ID)
assert not entity_state

# Update and test that entity is now enabled
updated_entry = entity_registry.async_update_entity(
TEST_BINARY_SENSOR_STEPS_ALL_OCCUPANCY_ENTITY_ID, disabled_by=None
await test_entities_are_setup_correctly_in_registry(
hass,
entities_enabled={
TEST_BINARY_SENSOR_FRONT_DOOR_MOTION_ENTITY_ID,
TEST_BINARY_SENSOR_FRONT_DOOR_PERSON_OCCUPANCY_ENTITY_ID,
TEST_BINARY_SENSOR_FRONT_DOOR_ALL_OCCUPANCY_ENTITY_ID,
TEST_BINARY_SENSOR_STEPS_ALL_OCCUPANCY_ENTITY_ID,
TEST_BINARY_SENSOR_STEPS_PERSON_OCCUPANCY_ENTITY_ID,
},
entities_visible={
TEST_BINARY_SENSOR_FRONT_DOOR_MOTION_ENTITY_ID,
TEST_BINARY_SENSOR_FRONT_DOOR_PERSON_OCCUPANCY_ENTITY_ID,
TEST_BINARY_SENSOR_STEPS_PERSON_OCCUPANCY_ENTITY_ID,
TEST_BINARY_SENSOR_FRONT_DOOR_ALL_OCCUPANCY_ENTITY_ID,
TEST_BINARY_SENSOR_STEPS_ALL_OCCUPANCY_ENTITY_ID,
},
)
assert not updated_entry.disabled
20 changes: 20 additions & 0 deletions tests/test_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
create_mock_frigate_client,
create_mock_frigate_config_entry,
setup_mock_frigate_config_entry,
test_entities_are_setup_correctly_in_registry,
)

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -217,3 +218,22 @@ async def test_camera_option_stream_url_template(
source = await async_get_stream_source(hass, TEST_CAMERA_FRONT_DOOR_ENTITY_ID)
assert source
assert source == "rtmp://localhost/front_door"


async def test_cameras_setup_correctly_in_registry(
aiohttp_server: Any, hass: HomeAssistant
) -> None:
"""Verify entities are enabled/visible as appropriate."""

await setup_mock_frigate_config_entry(hass)
await test_entities_are_setup_correctly_in_registry(
hass,
entities_enabled={
TEST_CAMERA_FRONT_DOOR_ENTITY_ID,
TEST_CAMERA_FRONT_DOOR_PERSON_ENTITY_ID,
},
entities_visible={
TEST_CAMERA_FRONT_DOOR_ENTITY_ID,
TEST_CAMERA_FRONT_DOOR_PERSON_ENTITY_ID,
},
)
Loading