Skip to content

Commit

Permalink
💥 Support for general motion sensor (#248)
Browse files Browse the repository at this point in the history
* Update name to make functionality more clear

* Add new motion sensor type

* Use right list operator

* Add tests for general motion sensor

* Fix pytest

* Fix incorrect id

* Entity needs to be enabled to test state

* Entity needs to be enabled to test state

* Await to ensure that entity is enabled

* Convert existing 'motion' sensor to presence sensor

* Update tests and test vars

* Fix other entity naming

* Reorganize tests

* Small cleanups

* Remove bad test

* presence -> occupancy

* Update tests for occupancy

* Fix testing

* Fix testing

* Update test to fire mqtt

* Adjust payload for changes upstream

* Adjust test to fit updated payload

* Update to new topic from parent PR

* Get the tests to pass.

* Attempt to remove old motion entities

* Add test for removing old motion sensor

* Fix name of domain

* Fix interaction between v1_v2 migration and the motion sensor rename.

Co-authored-by: Dermot Duffy <dermot.duffy@gmail.com>
  • Loading branch information
NickM-27 and dermotduffy committed May 19, 2022
1 parent 1bdaa33 commit 8deb6a0
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 36 deletions.
25 changes: 24 additions & 1 deletion custom_components/frigate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ def get_friendly_name(name: str) -> str:
return name.replace("_", " ").title()


def get_cameras(config: dict[str, Any]) -> set[str]:
"""Get cameras."""
cameras = set()

for cam_name, _ in config["cameras"].items():
cameras.add(cam_name)

return cameras


def get_cameras_and_objects(
config: dict[str, Any], include_all: bool = True
) -> set[tuple[str, str]]:
Expand Down Expand Up @@ -232,6 +242,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
new_options.pop(CONF_CAMERA_STATIC_IMAGE_HEIGHT)
hass.config_entries.async_update_entry(entry, options=new_options)

# Cleanup object_motion sensors (replaced with occupancy sensors).
for cam_name, obj_name in get_cameras_zones_and_objects(config):
unique_id = get_frigate_entity_unique_id(
entry.entry_id,
"motion_sensor",
f"{cam_name}_{obj_name}",
)
entity_id = entity_registry.async_get_entity_id(
"binary_sensor", DOMAIN, unique_id
)
if entity_id:
entity_registry.async_remove(entity_id)

hass.config_entries.async_setup_platforms(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(_async_entry_updated))

Expand Down Expand Up @@ -289,7 +312,7 @@ def update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None:

converters: Final[dict[re.Pattern, Callable[[re.Match], list[str]]]] = {
re.compile(rf"^{DOMAIN}_(?P<cam_obj>\S+)_binary_sensor$"): lambda m: [
"motion_sensor",
"occupancy_sensor",
m.group("cam_obj"),
],
re.compile(rf"^{DOMAIN}_(?P<cam>\S+)_camera$"): lambda m: [
Expand Down
105 changes: 98 additions & 7 deletions custom_components/frigate/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from homeassistant.components.binary_sensor import (
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
Expand All @@ -16,6 +17,7 @@
from . import (
FrigateMQTTEntity,
ReceiveMessage,
get_cameras,
get_cameras_zones_and_objects,
get_friendly_name,
get_frigate_device_identifier,
Expand All @@ -32,16 +34,30 @@ async def async_setup_entry(
) -> None:
"""Binary sensor entry setup."""
frigate_config = hass.data[DOMAIN][entry.entry_id][ATTR_CONFIG]
async_add_entities(

entities = []

# add object sensors for cameras and zones
entities.extend(
[
FrigateMotionSensor(entry, frigate_config, cam_name, obj)
FrigateObjectOccupancySensor(entry, frigate_config, cam_name, obj)
for cam_name, obj in get_cameras_zones_and_objects(frigate_config)
]
)

# add generic motion sensors for cameras
entities.extend(
[
FrigateMotionSensor(entry, frigate_config, cam_name)
for cam_name in get_cameras(frigate_config)
]
)

async_add_entities(entities)

class FrigateMotionSensor(FrigateMQTTEntity, BinarySensorEntity): # type: ignore[misc]
"""Frigate Motion Sensor class."""

class FrigateObjectOccupancySensor(FrigateMQTTEntity, BinarySensorEntity): # type: ignore[misc]
"""Frigate Occupancy Sensor class."""

def __init__(
self,
Expand All @@ -50,7 +66,7 @@ def __init__(
cam_name: str,
obj_name: str,
) -> None:
"""Construct a new FrigateMotionSensor."""
"""Construct a new FrigateObjectOccupancySensor."""
self._cam_name = cam_name
self._obj_name = obj_name
self._is_on = False
Expand Down Expand Up @@ -81,7 +97,7 @@ def unique_id(self) -> str:
"""Return a unique ID for this entity."""
return get_frigate_entity_unique_id(
self._config_entry.entry_id,
"motion_sensor",
"occupancy_sensor",
f"{self._cam_name}_{self._obj_name}",
)

Expand All @@ -102,7 +118,7 @@ def device_info(self) -> dict[str, Any]:
@property
def name(self) -> str:
"""Return the name of the sensor."""
return f"{get_friendly_name(self._cam_name)} {self._obj_name} Motion".title()
return f"{get_friendly_name(self._cam_name)} {self._obj_name} Occupancy".title()

@property
def is_on(self) -> bool:
Expand All @@ -114,6 +130,81 @@ 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."""
return cast(str, DEVICE_CLASS_OCCUPANCY)


class FrigateMotionSensor(FrigateMQTTEntity, BinarySensorEntity): # type: ignore[misc]
"""Frigate Motion Sensor class."""

def __init__(
self,
config_entry: ConfigEntry,
frigate_config: dict[str, Any],
cam_name: str,
) -> None:
"""Construct a new FrigateMotionSensor."""
self._cam_name = cam_name
self._is_on = False
self._frigate_config = frigate_config

super().__init__(
config_entry,
frigate_config,
{
"topic": (
f"{frigate_config['mqtt']['topic_prefix']}"
f"/{self._cam_name}/motion"
)
},
)

@callback # type: ignore[misc]
def _state_message_received(self, msg: ReceiveMessage) -> None:
"""Handle a new received MQTT state message."""
self._is_on = msg.payload == "ON"
super()._state_message_received(msg)

@property
def unique_id(self) -> str:
"""Return a unique ID for this entity."""
return get_frigate_entity_unique_id(
self._config_entry.entry_id,
"motion_sensor",
f"{self._cam_name}_motion",
)

@property
def device_info(self) -> dict[str, Any]:
"""Return device information."""
return {
"identifiers": {
get_frigate_device_identifier(self._config_entry, self._cam_name)
},
"via_device": get_frigate_device_identifier(self._config_entry),
"name": get_friendly_name(self._cam_name),
"model": self._get_model(),
"configuration_url": f"{self._config_entry.data.get(CONF_URL)}/cameras/{self._cam_name if self._cam_name not in get_zones(self._frigate_config) else ''}",
"manufacturer": NAME,
}

@property
def name(self) -> str:
"""Return the name of the sensor."""
return f"{get_friendly_name(self._cam_name)} Motion".title()

@property
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
11 changes: 7 additions & 4 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant

TEST_BINARY_SENSOR_FRONT_DOOR_PERSON_MOTION_ENTITY_ID = (
"binary_sensor.front_door_person_motion"
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_STEPS_PERSON_MOTION_ENTITY_ID = "binary_sensor.steps_person_motion"
TEST_BINARY_SENSOR_STEPS_ALL_MOTION_ENTITY_ID = "binary_sensor.steps_all_motion"
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"
Expand Down

0 comments on commit 8deb6a0

Please sign in to comment.