From 49529a144a491ec1c41e3382560ddd70c236f882 Mon Sep 17 00:00:00 2001 From: Maciej <38075949+maciej-or@users.noreply.github.com> Date: Thu, 11 Jul 2024 18:52:36 +0200 Subject: [PATCH] entities translation support (#184) * entities translation support * hassfest fix * title case for english entities --- README.md | 5 +- .../hikvision_next/binary_sensor.py | 6 +- custom_components/hikvision_next/camera.py | 12 +- custom_components/hikvision_next/const.py | 8 +- custom_components/hikvision_next/sensor.py | 19 +-- custom_components/hikvision_next/switch.py | 19 +-- .../hikvision_next/translations/en.json | 96 +++++++++++++- .../hikvision_next/translations/pl.json | 119 ++++++++++++++++++ tests/test_camera.py | 22 +++- 9 files changed, 271 insertions(+), 35 deletions(-) create mode 100644 custom_components/hikvision_next/translations/pl.json diff --git a/README.md b/README.md index ceef04f..bb383d4 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The Home Assistant integration for Hikvision NVRs and IP cameras. Receives and s - Switches for NVR Outputs - Holiday mode switch (allows to switch continuous recording with appropriate NVR setup) - Tracking HDD and NAS status -- Tracking Alarm Server settings for diagnostic purposes +- Tracking Notifications Host settings for diagnostic purposes - Basic and digest authentication support ### Supported events @@ -66,7 +66,7 @@ The scope supported features depends on device model, setup and firmware version - Regions if needed - Arming Schedule - Storage Schedule Settings - set continuous recording in Holiday mode for desired cameras -- Alarm Server - IP address of Home Assistant instance for event notifications. Can be set manually or by this integration if checked `Set alarm server` checkbox in the configuration dialog. It will be reverted to `http://0.0.0.0:80/` on integration unload. +- Notifications Host - IP address of Home Assistant instance for event notifications. Can be set manually or by this integration if checked `Set Notifications Host` checkbox in the configuration dialog. It will be reverted to `http://0.0.0.0:80/` on integration unload. ## Reporting issues @@ -99,6 +99,7 @@ Download logs from `Settings / System / Logs` - DS-7608NXI-K1/8P - DS-7616NI-E2/16P - DS-7616NI-I2/16P +- DS-7616NI-Q2/16P - DS-7616NXI-I2/16P/S - DS-7716NI-I4/16P - ERI-K104-P4 diff --git a/custom_components/hikvision_next/binary_sensor.py b/custom_components/hikvision_next/binary_sensor.py index 91c9bc3..d455651 100644 --- a/custom_components/hikvision_next/binary_sensor.py +++ b/custom_components/hikvision_next/binary_sensor.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_ISAPI, DOMAIN, EVENTS +from .const import DATA_ISAPI, DOMAIN, EVENTS, EVENT_IO from .isapi import EventInfo @@ -42,6 +42,8 @@ def __init__(self, isapi, device_id: int, event: EventInfo) -> None: """Initialize.""" self.entity_id = ENTITY_ID_FORMAT.format(event.unique_id) self._attr_unique_id = self.entity_id - self._attr_name = f"{EVENTS[event.id]['label']}{' ' + str(event.io_port_id) if event.io_port_id != 0 else ''}" + self._attr_translation_key = event.id + if event.id == EVENT_IO: + self._attr_translation_placeholders = {"io_port_id": event.io_port_id} self._attr_device_class = EVENTS[event.id]["device_class"] self._attr_device_info = isapi.hass_device_info(device_id) diff --git a/custom_components/hikvision_next/camera.py b/custom_components/hikvision_next/camera.py index f50c88b..da5cac9 100644 --- a/custom_components/hikvision_next/camera.py +++ b/custom_components/hikvision_next/camera.py @@ -41,8 +41,16 @@ def __init__( Camera.__init__(self) self._attr_device_info = isapi.hass_device_info(camera.id) - self._attr_name = f"{camera.name} {stream_info.type}" - self._attr_unique_id = slugify(f"{isapi.device_info.serial_no.lower()}_{stream_info.id}") + self._attr_unique_id = slugify( + f"{isapi.device_info.serial_no.lower()}_{stream_info.id}" + ) + if stream_info.type_id > 1: + self._attr_has_entity_name = True + self._attr_translation_key = f"stream{stream_info.type_id}" + self._attr_entity_registry_enabled_default = False + else: + # for the main stream use just its name + self._attr_name = camera.name self.entity_id = f"camera.{self.unique_id}" self.isapi = isapi self.stream_info = stream_info diff --git a/custom_components/hikvision_next/const.py b/custom_components/hikvision_next/const.py index b9336a2..af4a3f8 100644 --- a/custom_components/hikvision_next/const.py +++ b/custom_components/hikvision_next/const.py @@ -15,10 +15,6 @@ SECONDARY_COORDINATOR: Final = "secondary" HOLIDAY_MODE = "holiday_mode" -EVENT_SWITCH_LABEL_FORMAT = "{} Detection" -HOLIDAY_MODE_SWITCH_LABEL = "Holiday mode" -ALARM_SERVER_SENSOR_LABEL_FORMAT = "Alarm Server {}" - CONNECTION_TYPE_DIRECT = "Direct" CONNECTION_TYPE_PROXIED = "Proxied" @@ -85,8 +81,8 @@ "slug": "inputs", "direct_node": "IOInputPort", "proxied_node": "IOProxyInputPort", - "device_class": BinarySensorDeviceClass.MOTION - } + "device_class": BinarySensorDeviceClass.MOTION, + }, } EVENTS_ALTERNATE_ID = { diff --git a/custom_components/hikvision_next/sensor.py b/custom_components/hikvision_next/sensor.py index 039486c..4842264 100644 --- a/custom_components/hikvision_next/sensor.py +++ b/custom_components/hikvision_next/sensor.py @@ -10,7 +10,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( - ALARM_SERVER_SENSOR_LABEL_FORMAT, DATA_ALARM_SERVER_HOST, DOMAIN, EVENTS_COORDINATOR, @@ -18,11 +17,11 @@ ) from .isapi import StorageInfo -ALARM_SERVER_SETTINGS = { - "protocolType": "Protocol", - "ipAddress": "IP", - "portNo": "Port", - "url": "Path", +NOTIFICATION_HOST_KEYS = { + "protocolType": "protocol_type", + "ipAddress": "ip_address", + "portNo": "port_no", + "url": "url", } @@ -38,7 +37,7 @@ async def async_setup_entry( entities = [] if coordinator: - for key in ALARM_SERVER_SETTINGS: + for key in NOTIFICATION_HOST_KEYS: entities.append(AlarmServerSensor(coordinator, key)) events_coordinator = config.get(EVENTS_COORDINATOR) @@ -60,10 +59,12 @@ def __init__(self, coordinator, key: str) -> None: """Initialize.""" super().__init__(coordinator) isapi = coordinator.isapi - self._attr_unique_id = f"{isapi.device_info.serial_no}_{DATA_ALARM_SERVER_HOST}_{key}" + self._attr_unique_id = ( + f"{isapi.device_info.serial_no}_{DATA_ALARM_SERVER_HOST}_{key}" + ) self.entity_id = ENTITY_ID_FORMAT.format(self.unique_id) self._attr_device_info = isapi.hass_device_info() - self._attr_name = ALARM_SERVER_SENSOR_LABEL_FORMAT.format(ALARM_SERVER_SETTINGS[key]) + self._attr_translation_key = f"notifications_host_{NOTIFICATION_HOST_KEYS[key]}" self.key = key @property diff --git a/custom_components/hikvision_next/switch.py b/custom_components/hikvision_next/switch.py index c8769b8..287ae29 100644 --- a/custom_components/hikvision_next/switch.py +++ b/custom_components/hikvision_next/switch.py @@ -13,12 +13,10 @@ from .const import ( DOMAIN, - EVENT_SWITCH_LABEL_FORMAT, - EVENTS, EVENTS_COORDINATOR, HOLIDAY_MODE, - HOLIDAY_MODE_SWITCH_LABEL, SECONDARY_COORDINATOR, + EVENT_IO, ) from .isapi import EventInfo @@ -65,9 +63,9 @@ def __init__(self, device_id: int, event: EventInfo, coordinator) -> None: self.entity_id = ENTITY_ID_FORMAT.format(event.unique_id) self._attr_unique_id = self.entity_id self._attr_device_info = coordinator.isapi.hass_device_info(device_id) - self._attr_name = EVENT_SWITCH_LABEL_FORMAT.format( - f"{EVENTS[event.id]['label']}{' ' + str(event.io_port_id) if event.io_port_id != 0 else ''}" - ) + self._attr_translation_key = event.id + if event.id == EVENT_IO: + self._attr_translation_placeholders = {"io_port_id": event.io_port_id} self.device_id = device_id self.event = event @@ -107,6 +105,7 @@ class NVROutputSwitch(CoordinatorEntity, SwitchEntity): _attr_has_entity_name = True _attr_icon = "mdi:eye-outline" + _attr_translation_key = "alarm_output" def __init__(self, coordinator, port_no: int) -> None: """Initialize.""" @@ -116,7 +115,7 @@ def __init__(self, coordinator, port_no: int) -> None: ) self._attr_unique_id = self.entity_id self._attr_device_info = coordinator.isapi.hass_device_info(0) - self._attr_name = f"Alarm Output {port_no}" + self._attr_translation_placeholders = {"port_no": port_no} self._port_no = port_no @property @@ -147,14 +146,16 @@ class HolidaySwitch(CoordinatorEntity, SwitchEntity): _attr_has_entity_name = True _attr_icon = "mdi:palm-tree" + _attr_translation_key = HOLIDAY_MODE def __init__(self, coordinator) -> None: """Initialize.""" super().__init__(coordinator) - self._attr_unique_id = f"{slugify(coordinator.isapi.device_info.serial_no.lower())}_{HOLIDAY_MODE}" + self._attr_unique_id = ( + f"{slugify(coordinator.isapi.device_info.serial_no.lower())}_{HOLIDAY_MODE}" + ) self.entity_id = ENTITY_ID_FORMAT.format(self.unique_id) self._attr_device_info = coordinator.isapi.hass_device_info() - self._attr_name = HOLIDAY_MODE_SWITCH_LABEL @property def is_on(self) -> bool | None: diff --git a/custom_components/hikvision_next/translations/en.json b/custom_components/hikvision_next/translations/en.json index 69fdb0f..1d738ed 100644 --- a/custom_components/hikvision_next/translations/en.json +++ b/custom_components/hikvision_next/translations/en.json @@ -10,7 +10,7 @@ "host": "URL", "password": "Password", "username": "Username", - "set_alarm_server": "Set alarm server using following address:", + "set_alarm_server": "Set notifications host using following address:", "alarm_server": "Home Assistant address accessible by Hikvison device" } } @@ -21,5 +21,99 @@ "insufficient_permission": "Access forbidden, check user permissions", "unknown": "Unexpected error" } + }, + "entity": { + "binary_sensor": { + "motiondetection": { + "name": "Motion" + }, + "tamperdetection": { + "name": "Video Tampering" + }, + "videoloss": { + "name": "Video Loss" + }, + "scenechangedetection": { + "name": "Scene Change" + }, + "fielddetection": { + "name": "Intrusion" + }, + "linedetection": { + "name": "Line Crossing" + }, + "regionentrance": { + "name": "Region Entrance" + }, + "regionexiting": { + "name": "Region Exiting" + }, + "io": { + "name": "Alarm Input {io_port_id}" + } + }, + "switch": { + "alarm_output": { + "name": "Alarm Output {port_no}" + }, + "holiday_mode": { + "name": "Holiday Mode" + }, + "motiondetection": { + "name": "Motion Detection" + }, + "tamperdetection": { + "name": "Video Tampering Detection" + }, + "videoloss": { + "name": "Video Loss Detection" + }, + "scenechangedetection": { + "name": "Scene Change Detection" + }, + "fielddetection": { + "name": "Intrusion Detection" + }, + "linedetection": { + "name": "Line Crossing Detection" + }, + "regionentrance": { + "name": "Region Entrance Detection" + }, + "regionexiting": { + "name": "Region Exiting Detection" + }, + "io": { + "name": "Alarm Input {io_port_id}" + } + }, + "camera": { + "stream1": { + "name": "Main Stream" + }, + "stream2": { + "name": "Sub-Stream" + }, + "stream3": { + "name": "Main Stream (Event)" + }, + "stream4": { + "name": "Transcoded Stream" + } + }, + "sensor": { + "notifications_host_protocol_type": { + "name": "Notifications Host Protocol" + }, + "notifications_host_ip_address": { + "name": "Notifications Host IP" + }, + "notifications_host_port_no": { + "name": "Notifications Host Port" + }, + "notifications_host_url": { + "name": "Notifications Host Path" + } + } } } diff --git a/custom_components/hikvision_next/translations/pl.json b/custom_components/hikvision_next/translations/pl.json new file mode 100644 index 0000000..dcffbe2 --- /dev/null +++ b/custom_components/hikvision_next/translations/pl.json @@ -0,0 +1,119 @@ +{ + "config": { + "abort": { + "already_configured": "Urządzenie jest już skonfigurowane" + }, + "step": { + "user": { + "description": "Proszę wprowadzić dane logowania do NVR lub kamery IP", + "data": { + "host": "URL", + "password": "Hasło", + "username": "Nazwa użytkownika", + "set_alarm_server": "Ustaw host powiadomień używając następującego adresu:", + "alarm_server": "Adres Home Assistant dostępny dla urządzenia Hikvision" + } + } + }, + "error": { + "cannot_connect": "Nie udało się połączyć", + "invalid_auth": "Nieprawidłowa autoryzacja", + "insufficient_permission": "Dostęp zabroniony, sprawdź uprawnienia użytkownika", + "unknown": "Nieoczekiwany błąd" + } + }, + "entity": { + "binary_sensor": { + "motiondetection": { + "name": "Ruch" + }, + "tamperdetection": { + "name": "Sabotaż obrazu" + }, + "videoloss": { + "name": "Zanik sygnału wideo" + }, + "scenechangedetection": { + "name": "Zmiana sceny" + }, + "fielddetection": { + "name": "Wtargnięcie" + }, + "linedetection": { + "name": "Przekroczenie linii" + }, + "regionentrance": { + "name": "Wejście w obszar" + }, + "regionexiting": { + "name": "Wyjście z obszaru" + }, + "io": { + "name": "Wejście alarmowe {io_port_id}" + } + }, + "switch": { + "alarm_output": { + "name": "Wyjście alarmowe {port_no}" + }, + "holiday_mode": { + "name": "Tryb urlopowy" + }, + "motiondetection": { + "name": "Detekcja ruchu" + }, + "tamperdetection": { + "name": "Detekcja sabotażu obrazu" + }, + "videoloss": { + "name": "Detekcja zaniku sygnału wideo" + }, + "scenechangedetection": { + "name": "Detekcja zmiany sceny" + }, + "fielddetection": { + "name": "Detekcja wtargnięcia" + }, + "linedetection": { + "name": "Detekcja przekroczenia linii" + }, + "regionentrance": { + "name": "Detekcja wejścia w obszar" + }, + "regionexiting": { + "name": "Detekcja wyjścia z obszaru" + }, + "io": { + "name": "Wejście alarmowe {io_port_id}" + } + }, + "camera": { + "stream1": { + "name": "Strumień główny" + }, + "stream2": { + "name": "Podstrumień" + }, + "stream3": { + "name": "Strumień główny (zdarzenie)" + }, + "stream4": { + "name": "Strumień transkodowany" + } + }, + "sensor": { + "notifications_host_protocol_type": { + "name": "Host powiadomień protokół" + }, + "notifications_host_ip_address": { + "name": "Host powiadomień IP" + }, + "notifications_host_port_no": { + "name": "Host powiadomień port" + }, + "notifications_host_url": { + "name": "Host powiadomień ścieżka" + } + } + } +} diff --git a/tests/test_camera.py b/tests/test_camera.py index 5c3fad4..b697ce4 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -10,23 +10,37 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry from tests.conftest import load_fixture from tests.conftest import TEST_HOST +import homeassistant.helpers.entity_registry as er @pytest.mark.parametrize("init_integration", ["DS-7608NXI-I2"], indirect=True) async def test_camera(hass: HomeAssistant, init_integration: MockConfigEntry) -> None: """Test camera initialization.""" - assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 9 + assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 3 entity_id = "camera.ds_7608nxi_i0_0p_s0000000000ccrrj00000000wcvu_101" - assert (camera := hass.states.get(entity_id)) - assert camera.state == STATE_IDLE - assert camera.name == "garden Main Stream" + assert hass.states.get(entity_id) camera_entity = camera_component._get_camera_from_entity_id(hass, entity_id) + assert camera_entity.state == STATE_IDLE + assert camera_entity.name == "garden" + stream_url = await camera_entity.stream_source() assert stream_url == "rtsp://u1:%2A%2A%2A@1.0.0.255:10554/Streaming/channels/101" + entity_id = "camera.ds_7608nxi_i0_0p_s0000000000ccrrj00000000wcvu_102" + entity_registry = er.async_get(hass) + camera_entity = entity_registry.async_get(entity_id) + assert camera_entity.disabled + assert camera_entity.original_name == "Sub-Stream" + + entity_id = "camera.ds_7608nxi_i0_0p_s0000000000ccrrj00000000wcvu_104" + entity_registry = er.async_get(hass) + camera_entity = entity_registry.async_get(entity_id) + assert camera_entity.disabled + assert camera_entity.original_name == "Transcoded Stream" + @respx.mock @pytest.mark.parametrize("init_integration", ["DS-7608NXI-I2"], indirect=True)