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

💥 Support for general motion sensor #248

Merged
merged 29 commits into from
May 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6863cbf
Update name to make functionality more clear
NickM-27 Apr 27, 2022
80abf30
Add new motion sensor type
NickM-27 Apr 27, 2022
2cab9a8
Use right list operator
NickM-27 Apr 27, 2022
aba7dfb
Add tests for general motion sensor
NickM-27 Apr 27, 2022
7cac665
Fix pytest
NickM-27 Apr 27, 2022
11befe7
Fix incorrect id
NickM-27 Apr 27, 2022
f8754c8
Entity needs to be enabled to test state
NickM-27 Apr 27, 2022
8489185
Entity needs to be enabled to test state
NickM-27 Apr 27, 2022
05d23db
Await to ensure that entity is enabled
NickM-27 Apr 27, 2022
d74cf12
Convert existing 'motion' sensor to presence sensor
NickM-27 May 1, 2022
5d483bb
Update tests and test vars
NickM-27 May 1, 2022
b434be0
Fix other entity naming
NickM-27 May 1, 2022
c5062cc
Reorganize tests
NickM-27 May 1, 2022
606d5ea
Small cleanups
NickM-27 May 1, 2022
bc6f49c
Remove bad test
NickM-27 May 1, 2022
c831108
presence -> occupancy
NickM-27 May 1, 2022
315965e
Update tests for occupancy
NickM-27 May 1, 2022
0ecba6d
Fix testing
NickM-27 May 1, 2022
8a4d790
Fix testing
NickM-27 May 1, 2022
9136c62
Update test to fire mqtt
NickM-27 May 1, 2022
95b973a
Merge branch 'master' of github.com:blakeblackshear/frigate-hass-inte…
NickM-27 May 13, 2022
93fb3c8
Adjust payload for changes upstream
NickM-27 May 13, 2022
aa8eef0
Adjust test to fit updated payload
NickM-27 May 13, 2022
9170565
Update to new topic from parent PR
NickM-27 May 17, 2022
a525ed2
Get the tests to pass.
dermotduffy May 18, 2022
8db63c7
Attempt to remove old motion entities
NickM-27 May 18, 2022
c9bb18d
Add test for removing old motion sensor
NickM-27 May 18, 2022
6a727bd
Fix name of domain
NickM-27 May 18, 2022
9f1bbb1
Fix interaction between v1_v2 migration and the motion sensor rename.
dermotduffy May 19, 2022
File filter

Filter by extension

Filter by extension

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