diff --git a/blueprints/automation/hikvision_next/take_pictures_on_motion_detection.yaml b/blueprints/automation/hikvision_next/take_pictures_on_motion_detection.yaml new file mode 100755 index 0000000..8b0def2 --- /dev/null +++ b/blueprints/automation/hikvision_next/take_pictures_on_motion_detection.yaml @@ -0,0 +1,48 @@ +blueprint: + name: Take Multiple Snapshots On Detection Event + description: | + Takes pictures from multiple cameras when motion is detected on sensors. + Pictures are saved into /media/hikvision_next/snapshots folder + domain: automation + input: + motion_entities: + name: Sensor + description: An event sensor + selector: + entity: + multiple: true + domain: binary_sensor + device_class: + - motion + integration: hikvision_next + camera_entities: + name: Camera + description: A camera that takes a photo + selector: + entity: + multiple: true + domain: camera + integration: hikvision_next +mode: single +variables: + camera_entities: !input camera_entities + month: "{{now().strftime('%Y-%m')}}" + day: "{{now().strftime('%d')}}" + timestamp: "{{ now().strftime('%Y-%m-%d__%H-%M-%S') }}" +trigger: + - platform: state + entity_id: !input motion_entities + to: "on" +condition: [] +action: + - repeat: + count: "{{ camera_entities | count }}" + sequence: + - variables: + camera_entity: "{{ camera_entities[repeat.index - 1] }}" + camera_name: "{{ states[camera_entity].name }}" + - service: camera.snapshot + data: + filename: /media/hikvision_next/snapshots/{{month}}/{{day}}/{{timestamp}}__{{camera_name}}.jpg + target: + entity_id: "{{ camera_entity }}" diff --git a/custom_components/hikvision_next/isapi.py b/custom_components/hikvision_next/isapi.py index be0b52d..29a4f40 100644 --- a/custom_components/hikvision_next/isapi.py +++ b/custom_components/hikvision_next/isapi.py @@ -63,7 +63,8 @@ class AlertInfo: event_id: str device_serial_no: Optional[str] mac: str = "" - + region_id: int = 0 + detection_target: Optional[str] = None @dataclass class MutexIssue: @@ -844,10 +845,13 @@ def parse_event_notification(xml: str) -> AlertInfo: # ISAPI: break if not entry: - raise ValueError("Cannot find ISAPI instance for device %s in %s", device_ip, instances_hosts) + raise ValueError(f"Cannot find ISAPI instance for device {device_ip} in {instances_hosts}") config = self.hass.data[DOMAIN][entry.entry_id] return config.get(DATA_ISAPI) @@ -118,6 +118,14 @@ async def parse_event_request(self, request: web.Request) -> str: xml = part.text if headers.get(CONTENT_TYPE) == CONTENT_TYPE_IMAGE: _LOGGER.debug("image found") + # Use camera.snapshot service instead + # from datetime import datetime + # import aiofiles + # now = datetime.now() + # filename = f"/media/{DOMAIN}/snapshots/{now.strftime('%Y-%m-%d_%H-%M-%S_%f')}.jpg" + # async with aiofiles.open(filename, "wb") as image_file: + # await image_file.write(part.content) + # await image_file.flush() if not xml: raise ValueError(f"Unexpected event Content-Type {content_type_header}") @@ -157,7 +165,6 @@ def trigger_sensor(self, xml: str) -> None: _LOGGER.debug("UNIQUE_ID: %s", unique_id) - # unique_id = f"binary_sensor.{slugify(serial_no)}_{alert.channel_id}" f"_{alert.event_id}" entity_registry = async_get(self.hass) entity_id = entity_registry.async_get_entity_id(Platform.BINARY_SENSOR, DOMAIN, unique_id) if entity_id: @@ -180,6 +187,9 @@ def fire_hass_event(self, alert: AlertInfo): "camera_name": camera_name, "event_id": alert.event_id, } + if alert.detection_target: + message["detection_target"] = alert.detection_target + message["region_id"] = alert.region_id self.hass.bus.fire( HIKVISION_EVENT, diff --git a/tests/fixtures/ISAPI/EventNotificationAlert/fielddetection_human.xml b/tests/fixtures/ISAPI/EventNotificationAlert/fielddetection_human.xml new file mode 100644 index 0000000..e676f45 --- /dev/null +++ b/tests/fixtures/ISAPI/EventNotificationAlert/fielddetection_human.xml @@ -0,0 +1,49 @@ + + 1.0.0.255 + ::ffff:192.168.2.5 + 80 + HTTP + ac:b9:2f:49:44:75 + 1 + 2022-01-31T13:53:05+01:00 + 1 + fielddetection + active + fielddetection alarm + + + 3 + 50 + + + 572 + 100 + + + 572 + 995 + + + 999 + 998 + + + 992 + 210 + + + human + + 0.6 + 0.215278 + 0.135937 + 0.505556 + + + + COUR NORD + binary + 1 + false + diff --git a/tests/fixtures/ISAPI/EventNotificationAlert/fielddetection_vehicle.xml b/tests/fixtures/ISAPI/EventNotificationAlert/fielddetection_vehicle.xml new file mode 100644 index 0000000..646e42b --- /dev/null +++ b/tests/fixtures/ISAPI/EventNotificationAlert/fielddetection_vehicle.xml @@ -0,0 +1,49 @@ + + 1.0.0.255 + ::ffff:192.168.2.5 + 80 + HTTP + ac:b9:2f:49:44:75 + 1 + 2022-01-19T07:51:17+01:00 + 1 + fielddetection + active + fielddetection alarm + + + 2 + 50 + + + 118 + 327 + + + 999 + 326 + + + 999 + 626 + + + 0 + 630 + + + vehicle + + 0.779688 + 0.409722 + 0.209375 + 0.459722 + + + + COUR NORD + binary + 1 + false + \ No newline at end of file diff --git a/tests/test_notifications.py b/tests/test_notifications.py index ea103ba..e84fd4e 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -2,8 +2,9 @@ import pytest from http import HTTPStatus -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, Event from custom_components.hikvision_next.notifications import EventNotificationsView +from custom_components.hikvision_next.const import HIKVISION_EVENT from pytest_homeassistant_custom_component.common import MockConfigEntry from unittest.mock import MagicMock from tests.conftest import load_fixture, TEST_HOST_IP @@ -35,6 +36,11 @@ async def test_nvr_intrusion_detection_alert( """Test incoming intrusion detection event alert from nvr.""" entity_id = "binary_sensor.ds_7608nxi_i0_0p_s0000000000ccrrj00000000wcvu_2_fielddetection" + bus_events = [] + def bus_event_listener(event: Event) -> None: + bus_events.append(event) + hass.bus.async_listen(HIKVISION_EVENT, bus_event_listener) + assert (sensor := hass.states.get(entity_id)) assert sensor.state == STATE_OFF @@ -46,6 +52,13 @@ async def test_nvr_intrusion_detection_alert( assert (sensor := hass.states.get(entity_id)) assert sensor.state == STATE_ON + await hass.async_block_till_done() + assert len(bus_events) == 1 + data = bus_events[0].data + assert data["channel_id"] == 2 + assert data["event_id"] == "fielddetection" + assert data["camera_name"] == "home" + @pytest.mark.parametrize("init_integration", ["DS-2CD2386G2-IU"], indirect=True) async def test_ipc_intrusion_detection_alert( @@ -54,6 +67,7 @@ async def test_ipc_intrusion_detection_alert( """Test incoming intrusion detection event alert from ip camera.""" entity_id = "binary_sensor.ds_2cd2386g2_iu00000000aawrj00000000_1_fielddetection" + assert (sensor := hass.states.get(entity_id)) assert sensor.state == STATE_OFF @@ -64,3 +78,47 @@ async def test_ipc_intrusion_detection_alert( assert response.status == HTTPStatus.OK assert (sensor := hass.states.get(entity_id)) assert sensor.state == STATE_ON + + +@pytest.mark.parametrize("init_integration", ["DS-2CD2146G2-ISU"], indirect=True) +async def test_field_detection_alert( + hass: HomeAssistant, init_integration: MockConfigEntry, +) -> None: + """Test incoming field detection event with detection target.""" + + entity_id = "binary_sensor.ds_2cd2146g2_isu00000000aawrg00000000_1_fielddetection" + bus_events = [] + def bus_event_listener(event: Event) -> None: + bus_events.append(event) + hass.bus.async_listen(HIKVISION_EVENT, bus_event_listener) + + view = EventNotificationsView(hass) + mock_request = mock_event_notification("fielddetection_human") + response = await view.post(mock_request) + + assert response.status == HTTPStatus.OK + assert (sensor := hass.states.get(entity_id)) + assert sensor.state == STATE_ON + + await hass.async_block_till_done() + assert len(bus_events) == 1 + data = bus_events[0].data + assert data["channel_id"] == 1 + assert data["event_id"] == "fielddetection" + assert data["detection_target"] == "human" + assert data["region_id"] == 3 + + mock_request = mock_event_notification("fielddetection_vehicle") + response = await view.post(mock_request) + + assert response.status == HTTPStatus.OK + assert (sensor := hass.states.get(entity_id)) + assert sensor.state == STATE_ON + + await hass.async_block_till_done() + assert len(bus_events) == 2 + data = bus_events[1].data + assert data["channel_id"] == 1 + assert data["event_id"] == "fielddetection" + assert data["detection_target"] == "vehicle" + assert data["region_id"] == 2