diff --git a/README.md b/README.md index 704f7b3..725f8eb 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ The Home Assistant integration for Hikvision NVRs and IP cameras. Receives and s - Camera entities for main and sub streams - Real-time Acusense events notifications through binary sensors and HA events (hikvision_next_event) - Switches for Acusense events detection -- Switches for NVR Outputs +- Switches for NVR Outputs and PIR sensor - Holiday mode switch (allows to switch continuous recording with appropriate NVR setup) +- Image entities for the latest snapshots - Tracking HDD and NAS status - Tracking Notifications Host settings for diagnostic purposes - Basic and digest authentication support @@ -26,10 +27,16 @@ The Home Assistant integration for Hikvision NVRs and IP cameras. Receives and s - Region Entrance - Region Exiting - NVR Input Triggers +- PIR **NOTE** Events must be set to alert the surveillance center in Linkage Action for Home Assistant to be notified. Otherwise related binary sensors and switches will appear as disabled entities. +### Blueprints + +Take Multiple Snapshots On Detection Event + +[](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https://github.com/maciej-or/hikvision_next/blob/main/blueprints/take_pictures_on_motion_detection.yaml) ## Preview ### IP Camera device view @@ -42,6 +49,8 @@ The scope supported features depends on device model, setup and firmware version ## Installation +[](https://my.home-assistant.io/redirect/hacs_repository/?owner=maciej-or&repository=hikvision_next&category=integration) + ### With HACS 1. This integration you will find in the default HACS store. Search for `Hikvision NVR / IP Camera` on `HACS / Integrations` page and press `Download` button @@ -93,6 +102,7 @@ Download logs from `Settings / System / Logs` ### NVR +- Annke N46PCK - DS-7108NI-Q1/8P - DS-7608NI-I2 - DS-7608NI-I2/8P @@ -100,10 +110,10 @@ Download logs from `Settings / System / Logs` - DS-7608NXI-K1/8P - DS-7616NI-E2/16P - DS-7616NI-I2/16P +- DS-7616NI-Q2 - DS-7616NI-Q2/16P - DS-7616NXI-I2/16P/S - DS-7716NI-I4/16P -- Annke N46PCK - ERI-K104-P4 ### DVR @@ -113,8 +123,10 @@ Download logs from `Settings / System / Logs` ### IP Camera +- Annke C800 (I91BM) - DS-2CD2047G2-LU/SL - DS-2CD2087G2-LU +- DS-2CD2146G2-ISU - DS-2CD2155FWD-I - DS-2CD2346G2-IU - DS-2CD2386G2-IU @@ -127,4 +139,3 @@ Download logs from `Settings / System / Logs` - DS-2CD2T87G2-L - DS-2CD2T87G2P-LSU/SL - DS-2DE4425IW-DE (PTZ) -- Annke C800 (I91BM) diff --git a/blueprints/automation/hikvision_next/take_pictures_on_motion_detection.yaml b/blueprints/take_pictures_on_motion_detection.yaml similarity index 79% rename from blueprints/automation/hikvision_next/take_pictures_on_motion_detection.yaml rename to blueprints/take_pictures_on_motion_detection.yaml index 8b0def2..148e652 100755 --- a/blueprints/automation/hikvision_next/take_pictures_on_motion_detection.yaml +++ b/blueprints/take_pictures_on_motion_detection.yaml @@ -40,9 +40,15 @@ action: sequence: - variables: camera_entity: "{{ camera_entities[repeat.index - 1] }}" + snapshot_entity: "{{ camera_entities[repeat.index - 1] |regex_replace(find='camera', replace='image') }}_snapshot" 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 }}" + - service: hikvision_next.update_snapshot + data: + filename: /media/hikvision_next/snapshots/{{month}}/{{day}}/{{timestamp}}__{{camera_name}}.jpg + target: + entity_id: "{{ snapshot_entity }}" diff --git a/custom_components/hikvision_next/__init__.py b/custom_components/hikvision_next/__init__.py index 431ecd7..a93a0a3 100644 --- a/custom_components/hikvision_next/__init__.py +++ b/custom_components/hikvision_next/__init__.py @@ -8,14 +8,15 @@ from httpx import TimeoutException +from homeassistant.components.binary_sensor import ( + ENTITY_ID_FORMAT as BINARY_SENSOR_ENTITY_ID_FORMAT, +) +from homeassistant.components.switch import ENTITY_ID_FORMAT as SWITCH_ENTITY_ID_FORMAT from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers import entity_registry as er -from homeassistant.components.switch import ENTITY_ID_FORMAT as SWITCH_ENTITY_ID_FORMAT -from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT as BINARY_SENSOR_ENTITY_ID_FORMAT +from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import ( ALARM_SERVER_PATH, @@ -37,6 +38,7 @@ Platform.CAMERA, Platform.SENSOR, Platform.SWITCH, + Platform.IMAGE, ] diff --git a/custom_components/hikvision_next/image.py b/custom_components/hikvision_next/image.py new file mode 100644 index 0000000..719ef7d --- /dev/null +++ b/custom_components/hikvision_next/image.py @@ -0,0 +1,89 @@ +"""Image entities with camera snapshots.""" + +from datetime import datetime +import logging + +import voluptuous as vol + +from homeassistant.components.camera import Camera +from homeassistant.components.image import ImageEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, CONF_FILENAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template import Template +from homeassistant.util import slugify + +from .const import DATA_ISAPI, DOMAIN +from .isapi import ISAPI, CameraStreamInfo + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None: + """Add images with snapshots.""" + + config = hass.data[DOMAIN][entry.entry_id] + isapi: ISAPI = config[DATA_ISAPI] + + entities = [] + for camera in isapi.cameras: + for stream in camera.streams: + if stream.type_id == 1: + entities.append(SnapshotFile(hass, isapi, camera, stream)) + + async_add_entities(entities) + + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + "update_snapshot", + {vol.Required(CONF_FILENAME): cv.template}, + "update_snapshot_filename", + ) + + +class SnapshotFile(ImageEntity): + """An entity for displaying snapshot files.""" + + _attr_has_entity_name = True + file_path = None + + def __init__( + self, + hass: HomeAssistant, + isapi: ISAPI, + camera: Camera, + stream_info: CameraStreamInfo, + ) -> None: + """Initialize the snapshot file.""" + + ImageEntity.__init__(self, hass) + + self._attr_unique_id = slugify(f"{isapi.device_info.serial_no.lower()}_{stream_info.id}_snapshot") + self.entity_id = f"camera.{self.unique_id}" + self._attr_translation_key = "snapshot" + self._attr_translation_placeholders = {"camera": camera.name} + + def image(self) -> bytes | None: + """Return bytes of image.""" + try: + if self.file_path: + with open(self.file_path, "rb") as file: + return file.read() + except FileNotFoundError: + _LOGGER.warning( + "Could not read camera %s image from file: %s", + self.name, + self.file_path, + ) + return None + + async def update_snapshot_filename( + self, + filename: Template, + ) -> None: + """Update the file_path.""" + self.file_path = filename.async_render(variables={ATTR_ENTITY_ID: self.entity_id}) + self._attr_image_last_updated = datetime.now() + self.schedule_update_ha_state() diff --git a/custom_components/hikvision_next/services.yaml b/custom_components/hikvision_next/services.yaml new file mode 100644 index 0000000..e54d6cf --- /dev/null +++ b/custom_components/hikvision_next/services.yaml @@ -0,0 +1,15 @@ +update_snapshot: + name: Update snapshot + description: Update the snapshot file in an image entity. + target: + entity: + domain: image + integration: hikvision_next + fields: + filename: + required: true + name: Filename + description: The filename of the snapshot to display. + example: "/media/hikvision_next/snapshots/2024-08/07/2024-08-07__11-54-19__camera01.jpg" + selector: + text: diff --git a/custom_components/hikvision_next/translations/en.json b/custom_components/hikvision_next/translations/en.json index 1d738ed..3917540 100644 --- a/custom_components/hikvision_next/translations/en.json +++ b/custom_components/hikvision_next/translations/en.json @@ -114,6 +114,11 @@ "notifications_host_url": { "name": "Notifications Host Path" } + }, + "image": { + "snapshot": { + "name": "{camera} Snapshot" + } } } } diff --git a/custom_components/hikvision_next/translations/pl.json b/custom_components/hikvision_next/translations/pl.json index dcffbe2..8e115bf 100644 --- a/custom_components/hikvision_next/translations/pl.json +++ b/custom_components/hikvision_next/translations/pl.json @@ -114,6 +114,11 @@ "notifications_host_url": { "name": "Host powiadomień ścieżka" } + }, + "image": { + "snapshot": { + "name": "{camera} zdjęcie" + } } } }