From d64210943697b74a10edb2bc2f33b4665733dbb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Farkasdi?= <93778865+farkasdi@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:57:57 +0100 Subject: [PATCH] Netatmo NOCamera on/off fix (#158741) --- homeassistant/components/netatmo/camera.py | 19 +++++++++- homeassistant/components/netatmo/const.py | 3 ++ tests/components/netatmo/test_camera.py | 44 +++++++++++++++++++--- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index f21998bbac8..c8eab26d992 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -27,6 +27,8 @@ from .const import ( DATA_CAMERAS, DATA_EVENTS, DOMAIN, + EVENT_TYPE_CONNECTION, + EVENT_TYPE_DISCONNECTION, EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_OFF, EVENT_TYPE_ON, @@ -123,7 +125,13 @@ class NetatmoCamera(NetatmoModuleEntity, Camera): """Entity created.""" await super().async_added_to_hass() - for event_type in (EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_OFF, EVENT_TYPE_ON): + for event_type in ( + EVENT_TYPE_LIGHT_MODE, + EVENT_TYPE_OFF, + EVENT_TYPE_ON, + EVENT_TYPE_CONNECTION, + EVENT_TYPE_DISCONNECTION, + ): self.async_on_remove( async_dispatcher_connect( self.hass, @@ -146,12 +154,19 @@ class NetatmoCamera(NetatmoModuleEntity, Camera): data["home_id"] == self.home.entity_id and data["camera_id"] == self.device.entity_id ): - if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"): + if data[WEBHOOK_PUSH_TYPE] in ( + "NACamera-off", + "NOCamera-off", + "NACamera-disconnection", + "NOCamera-disconnection", + ): self._attr_is_streaming = False self._monitoring = False elif data[WEBHOOK_PUSH_TYPE] in ( "NACamera-on", + "NOCamera-on", WEBHOOK_NACAMERA_CONNECTION, + "NOCamera-connection", ): self._attr_is_streaming = True self._monitoring = True diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index d8ecc72ada7..bdd7fb99e7f 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -127,6 +127,9 @@ EVENT_TYPE_ALARM_STARTED = "alarm_started" EVENT_TYPE_DOOR_TAG_BIG_MOVE = "tag_big_move" EVENT_TYPE_DOOR_TAG_OPEN = "tag_open" EVENT_TYPE_DOOR_TAG_SMALL_MOVE = "tag_small_move" +# Generic events +EVENT_TYPE_CONNECTION = "connection" +EVENT_TYPE_DISCONNECTION = "disconnection" EVENT_TYPE_OFF = "off" EVENT_TYPE_ON = "on" diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 3411b3acd54..2f6c958e082 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -89,7 +89,7 @@ async def test_setup_component_with_webhook( assert hass.states.get(camera_entity_indoor).state == "streaming" - # Test outdoor camera events - not yet supported + # Test outdoor camera events assert hass.states.get(camera_entity_outdoor).state == "streaming" response = { "event_type": "off", @@ -100,8 +100,7 @@ async def test_setup_component_with_webhook( } await simulate_webhook(hass, webhook_id, response) - # The NOCamera-off push_type is not yet supported (assert should be "idle" when supported) - assert hass.states.get(camera_entity_outdoor).state == "streaming" + assert hass.states.get(camera_entity_outdoor).state == "idle" response = { "event_type": "on", @@ -425,8 +424,19 @@ async def test_service_set_camera_light_invalid_type( assert "NACamera does not have a floodlight" in excinfo.value.args[0] +@pytest.mark.parametrize( + ("camera_type", "camera_id", "camera_entity"), + [ + ("NACamera", "12:34:56:00:f1:62", "camera.hall"), + ("NOCamera", "12:34:56:10:b9:0e", "camera.front"), + ], +) async def test_camera_reconnect_webhook( - hass: HomeAssistant, config_entry: MockConfigEntry + hass: HomeAssistant, + config_entry: MockConfigEntry, + camera_type: str, + camera_id: str, + camera_entity: str, ) -> None: """Test webhook event on camera reconnect.""" fake_post_hits = 0 @@ -472,7 +482,7 @@ async def test_camera_reconnect_webhook( # Fake camera reconnect response = { - "push_type": "NACamera-connection", + "push_type": f"{camera_type}-connection", } await simulate_webhook(hass, webhook_id, response) await hass.async_block_till_done() @@ -484,6 +494,30 @@ async def test_camera_reconnect_webhook( await hass.async_block_till_done() assert fake_post_hits >= calls + # Real camera disconnect + assert hass.states.get(camera_entity).state == "streaming" + response = { + "event_type": "disconnection", + "device_id": camera_id, + "camera_id": camera_id, + "event_id": "601dce1560abca1ebad9b723", + "push_type": f"{camera_type}-disconnection", + } + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(camera_entity).state == "idle" + + response = { + "event_type": "connection", + "device_id": camera_id, + "camera_id": camera_id, + "event_id": "646227f1dc0dfa000ec5f350", + "push_type": f"{camera_type}-connection", + } + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(camera_entity).state == "streaming" + async def test_webhook_person_event( hass: HomeAssistant, config_entry: MockConfigEntry, netatmo_auth: AsyncMock