From 6386b25cac878386fd58ec9871f8f9583178b966 Mon Sep 17 00:00:00 2001 From: Nick Mowen Date: Fri, 14 Jul 2023 08:10:27 -0600 Subject: [PATCH 1/7] Add sensor for audio types --- custom_components/frigate/__init__.py | 10 +++ custom_components/frigate/binary_sensor.py | 94 ++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/custom_components/frigate/__init__.py b/custom_components/frigate/__init__.py index dff25908..eff625de 100644 --- a/custom_components/frigate/__init__.py +++ b/custom_components/frigate/__init__.py @@ -115,6 +115,16 @@ def get_cameras_and_objects( return camera_objects +def get_cameras_and_audio(config: dict[str, Any]) -> set[tuple[str, str]]: + """Get cameras and audio tuples.""" + camera_audio = set() + for cam_name, cam_config in config["cameras"].items(): + for audio in cam_config.get("audio", {}).get("listen", []): + camera_audio.add((cam_name, audio)) + + return camera_audio + + def get_cameras_zones_and_objects(config: dict[str, Any]) -> set[tuple[str, str]]: """Get cameras/zones and tracking object tuples.""" camera_objects = get_cameras_and_objects(config) diff --git a/custom_components/frigate/binary_sensor.py b/custom_components/frigate/binary_sensor.py index 515a9610..d38bf84a 100644 --- a/custom_components/frigate/binary_sensor.py +++ b/custom_components/frigate/binary_sensor.py @@ -17,6 +17,7 @@ FrigateMQTTEntity, ReceiveMessage, get_cameras, + get_cameras_and_audio, get_cameras_zones_and_objects, get_friendly_name, get_frigate_device_identifier, @@ -45,6 +46,14 @@ async def async_setup_entry( ] ) + # add audio sensors for cameras + entities.extend( + [ + FrigateAudioSensor(entry, frigate_config, cam_name, audio) + for cam_name, audio in get_cameras_and_audio(frigate_config) + ] + ) + # add generic motion sensors for cameras entities.extend( [ @@ -141,6 +150,91 @@ def icon(self) -> str: return get_dynamic_icon_from_type(self._obj_name, self._is_on) +class FrigateAudioSensor(FrigateMQTTEntity, BinarySensorEntity): # type: ignore[misc] + """Frigate Audio Sensor class.""" + + def __init__( + self, + config_entry: ConfigEntry, + frigate_config: dict[str, Any], + cam_name: str, + audio_name: str, + ) -> None: + """Construct a new FrigateAudioSensor.""" + self._cam_name = cam_name + self._audio_name = audio_name + self._is_on = False + self._frigate_config = frigate_config + + super().__init__( + config_entry, + frigate_config, + { + "state_topic": { + "msg_callback": self._state_message_received, + "qos": 0, + "topic": ( + f"{self._frigate_config['mqtt']['topic_prefix']}" + f"/{self._cam_name}/audio/{self._audio_name}" + ), + "encoding": None, + }, + }, + ) + + @callback # type: ignore[misc] + def _state_message_received(self, msg: ReceiveMessage) -> None: + """Handle a new received MQTT state message.""" + try: + self._is_on = msg.payload == "ON" + except ValueError: + self._is_on = False + self.async_write_ha_state() + + @property + def unique_id(self) -> str: + """Return a unique ID for this entity.""" + return get_frigate_entity_unique_id( + self._config_entry.entry_id, + "audio_sensor", + f"{self._cam_name}_{self._audio_name}", + ) + + @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}", + "manufacturer": NAME, + } + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return f"{self._obj_name} sound" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self._is_on + + @property + def device_class(self) -> str: + """Return the device class.""" + return cast(str, BinarySensorDeviceClass.SOUND) + + @property + def icon(self) -> str: + """Return the icon of the sensor.""" + return get_dynamic_icon_from_type(self._obj_name, self._is_on) + + class FrigateMotionSensor(FrigateMQTTEntity, BinarySensorEntity): # type: ignore[misc] """Frigate Motion Sensor class.""" From b6a6bc870c012079e04c620d418f5fed1e9edece Mon Sep 17 00:00:00 2001 From: Nick Mowen Date: Fri, 14 Jul 2023 08:12:57 -0600 Subject: [PATCH 2/7] Use icon --- custom_components/frigate/binary_sensor.py | 2 +- custom_components/frigate/icons.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/frigate/binary_sensor.py b/custom_components/frigate/binary_sensor.py index d38bf84a..6548d62b 100644 --- a/custom_components/frigate/binary_sensor.py +++ b/custom_components/frigate/binary_sensor.py @@ -147,7 +147,7 @@ def device_class(self) -> str: @property def icon(self) -> str: """Return the icon of the sensor.""" - return get_dynamic_icon_from_type(self._obj_name, self._is_on) + return get_dynamic_icon_from_type("sound", self._is_on) class FrigateAudioSensor(FrigateMQTTEntity, BinarySensorEntity): # type: ignore[misc] diff --git a/custom_components/frigate/icons.py b/custom_components/frigate/icons.py index cfa78c63..eb35ff36 100644 --- a/custom_components/frigate/icons.py +++ b/custom_components/frigate/icons.py @@ -1,6 +1,7 @@ """Handles icons for different entity types.""" ICON_AUDIO = "mdi:ear-hearing" +ICON_AUDIO_OFF = "mdi:ear-hearing-off" ICON_BICYCLE = "mdi:bicycle" ICON_CAR = "mdi:car" ICON_CAT = "mdi:cat" @@ -32,6 +33,8 @@ def get_dynamic_icon_from_type(obj_type: str, is_on: bool) -> str: return ICON_CAR if is_on else ICON_CAR_OFF if obj_type == "dog": return ICON_DOG if is_on else ICON_DOG_OFF + if obj_type == "sound": + return ICON_AUDIO if is_on else ICON_AUDIO_OFF return ICON_DEFAULT_ON if is_on else ICON_DEFAULT_OFF From 4fb8d2ebe942beb45729bd0d5b44f6220a6ba148 Mon Sep 17 00:00:00 2001 From: Nick Mowen Date: Fri, 14 Jul 2023 08:15:10 -0600 Subject: [PATCH 3/7] Only add if enabled --- custom_components/frigate/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/frigate/__init__.py b/custom_components/frigate/__init__.py index eff625de..83e0d092 100644 --- a/custom_components/frigate/__init__.py +++ b/custom_components/frigate/__init__.py @@ -119,8 +119,9 @@ def get_cameras_and_audio(config: dict[str, Any]) -> set[tuple[str, str]]: """Get cameras and audio tuples.""" camera_audio = set() for cam_name, cam_config in config["cameras"].items(): - for audio in cam_config.get("audio", {}).get("listen", []): - camera_audio.add((cam_name, audio)) + if cam_config.get("audio", {}).get("enabled_in_config", False): + for audio in cam_config.get("audio", {}).get("listen", []): + camera_audio.add((cam_name, audio)) return camera_audio From 62e857edfb05dc9d2779e1fd1650528bc6c1f8d2 Mon Sep 17 00:00:00 2001 From: Nick Mowen Date: Mon, 24 Jul 2023 15:29:56 -0600 Subject: [PATCH 4/7] Add audio binary sensor tests --- tests/__init__.py | 1 + tests/test_binary_sensor.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/tests/__init__.py b/tests/__init__.py index d3e45a37..9d137bba 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -22,6 +22,7 @@ TEST_BINARY_SENSOR_FRONT_DOOR_PERSON_OCCUPANCY_ENTITY_ID = ( "binary_sensor.front_door_person_occupancy" ) +TEST_BINARY_SENSOR_FRONT_DOOR_SPEECH_ENTITY_ID = "binary_sensor.front_door_speech_sound" TEST_BINARY_SENSOR_FRONT_DOOR_ALL_OCCUPANCY_ENTITY_ID = ( "binary_sensor.front_door_all_occupancy" ) diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index 79f39b8d..5a1ee1f3 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -17,6 +17,7 @@ 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_FRONT_DOOR_SPEECH_ENTITY_ID, TEST_BINARY_SENSOR_STEPS_ALL_OCCUPANCY_ENTITY_ID, TEST_BINARY_SENSOR_STEPS_PERSON_OCCUPANCY_ENTITY_ID, TEST_CONFIG_ENTRY_ID, @@ -78,6 +79,38 @@ async def test_occupancy_binary_sensor_setup(hass: HomeAssistant) -> None: assert entity_state.state == "unavailable" +async def test_audio_binary_sensor_setup(hass: HomeAssistant) -> None: + """Verify a successful audio binary sensor setup.""" + await setup_mock_frigate_config_entry(hass) + + entity_state = hass.states.get(TEST_BINARY_SENSOR_FRONT_DOOR_SPEECH_ENTITY_ID) + assert entity_state + assert entity_state.state == "unavailable" + + async_fire_mqtt_message(hass, "frigate/available", "online") + await hass.async_block_till_done() + entity_state = hass.states.get(TEST_BINARY_SENSOR_FRONT_DOOR_SPEECH_ENTITY_ID) + assert entity_state + assert entity_state.state == "off" + + async_fire_mqtt_message(hass, "frigate/front_door/audio/speech", "ON") + await hass.async_block_till_done() + entity_state = hass.states.get(TEST_BINARY_SENSOR_FRONT_DOOR_SPEECH_ENTITY_ID) + assert entity_state + assert entity_state.state == "on" + + async_fire_mqtt_message(hass, "frigate/front_door/audio/speech", "not_valid") + await hass.async_block_till_done() + entity_state = hass.states.get(TEST_BINARY_SENSOR_FRONT_DOOR_SPEECH_ENTITY_ID) + assert entity_state + assert entity_state.state == "off" + + async_fire_mqtt_message(hass, "frigate/available", "offline") + entity_state = hass.states.get(TEST_BINARY_SENSOR_FRONT_DOOR_SPEECH_ENTITY_ID) + assert entity_state + assert entity_state.state == "unavailable" + + async def test_motion_binary_sensor_setup(hass: HomeAssistant) -> None: """Verify a successful motion binary sensor setup.""" await setup_mock_frigate_config_entry(hass) From e8a866295439b6bd7bb8dd0ecb30fdb15b17189b Mon Sep 17 00:00:00 2001 From: Nick Mowen Date: Mon, 24 Jul 2023 15:35:33 -0600 Subject: [PATCH 5/7] Fix sound --- custom_components/frigate/binary_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/frigate/binary_sensor.py b/custom_components/frigate/binary_sensor.py index 6548d62b..dfdb8860 100644 --- a/custom_components/frigate/binary_sensor.py +++ b/custom_components/frigate/binary_sensor.py @@ -147,7 +147,7 @@ def device_class(self) -> str: @property def icon(self) -> str: """Return the icon of the sensor.""" - return get_dynamic_icon_from_type("sound", self._is_on) + return get_dynamic_icon_from_type(self._obj_name, self._is_on) class FrigateAudioSensor(FrigateMQTTEntity, BinarySensorEntity): # type: ignore[misc] @@ -217,7 +217,7 @@ def device_info(self) -> dict[str, Any]: @property def name(self) -> str: """Return the name of the sensor.""" - return f"{self._obj_name} sound" + return f"{self._audio_name} sound" @property def is_on(self) -> bool: @@ -232,7 +232,7 @@ def device_class(self) -> str: @property def icon(self) -> str: """Return the icon of the sensor.""" - return get_dynamic_icon_from_type(self._obj_name, self._is_on) + return get_dynamic_icon_from_type("sound", self._is_on) class FrigateMotionSensor(FrigateMQTTEntity, BinarySensorEntity): # type: ignore[misc] From 469aa47b841c26a1be2cc80b3715f80942bcb4d4 Mon Sep 17 00:00:00 2001 From: Nick Mowen Date: Mon, 24 Jul 2023 15:43:40 -0600 Subject: [PATCH 6/7] Simplify check --- custom_components/frigate/binary_sensor.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/custom_components/frigate/binary_sensor.py b/custom_components/frigate/binary_sensor.py index dfdb8860..b2e647f2 100644 --- a/custom_components/frigate/binary_sensor.py +++ b/custom_components/frigate/binary_sensor.py @@ -185,10 +185,7 @@ def __init__( @callback # type: ignore[misc] def _state_message_received(self, msg: ReceiveMessage) -> None: """Handle a new received MQTT state message.""" - try: - self._is_on = msg.payload == "ON" - except ValueError: - self._is_on = False + self._is_on = msg.payload == "ON" self.async_write_ha_state() @property From 96f912bbcce92155009f9c2ce75160261a24738c Mon Sep 17 00:00:00 2001 From: Nick Mowen Date: Mon, 24 Jul 2023 16:19:37 -0600 Subject: [PATCH 7/7] Remove encoding --- custom_components/frigate/binary_sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/frigate/binary_sensor.py b/custom_components/frigate/binary_sensor.py index b2e647f2..260f4815 100644 --- a/custom_components/frigate/binary_sensor.py +++ b/custom_components/frigate/binary_sensor.py @@ -177,7 +177,6 @@ def __init__( f"{self._frigate_config['mqtt']['topic_prefix']}" f"/{self._cam_name}/audio/{self._audio_name}" ), - "encoding": None, }, }, )