Skip to content

Commit

Permalink
Merge pull request #275 from maciej-or/dev
Browse files Browse the repository at this point in the history
release v1.1.1
  • Loading branch information
maciej-or authored Dec 15, 2024
2 parents 52f7578 + f51203e commit 199c19a
Show file tree
Hide file tree
Showing 23 changed files with 3,845 additions and 77 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ They test specific ISAPI requests and use data from `tests/fixtures/ISAPI`. They

### The behavior of the Hikvision device in HomeAssistant

They initialize the entire device in the HomeAssistant environment and uses data from `tests/fixtures/devices`. Each JSON file contains all the responses to GET requests sent by the given device.
They initialize the entire device in the HomeAssistant environment and use data from `tests/fixtures/devices`. Each JSON file contains all the responses to GET requests sent by the given device.

The fixtures can be recorded for any device in the Device Info window by clicking DOWNLOAD DIAGNOSTICS button. All sensitive data such as MAC addresses, IPs, and serial numbers are anonymized so they can be safely made public.

Expand Down Expand Up @@ -128,4 +128,4 @@ If you want to debug the tests in Visual Studio Code:

4. add a breakpoint

5. run the "Docker: Python tests debug" job in VS Code
5. run the "Docker: Python tests debug" job in VS Code
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,14 @@ Download logs from `Settings / System / Logs`
- DS-7616NI-Q2/16P
- DS-7616NXI-I2/16P/S
- DS-7716NI-I4/16P
- DS-7732NI-M4
- ERI-K104-P4

### DVR

- iDS-7204HUHI-M1/P
- iDS-7204HUHI-M1/FA/A
- iDS-7204HUHI-M1/P
- iDS-7208HQHI-M1(A)/S(C)

### IP Camera

Expand All @@ -145,10 +147,14 @@ Download logs from `Settings / System / Logs`
- DS-2CD2387G2-LU
- DS-2CD2387G2H-LISU/SL
- DS-2CD2425FWD-IW
- DS-2CD2443G0-IW
- DS-2CD2532F-IWS
- DS-2CD2546G2-IS
- DS-2CD2747G2-LZS
- DS-2CD2785G1-IZS
- DS-2CD2H46G2-IZS (C)
- DS-2CD2T46G2-ISU/SL
- DS-2CD2T87G2-L
- DS-2CD2T87G2P-LSU/SL
- DS-2DE4425IW-DE (PTZ)
- DS-2SE4C425MWG-E/26
4 changes: 3 additions & 1 deletion custom_components/hikvision_next/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
entity_registry = er.async_get(hass)
for key in old_keys:
entity_id = f"sensor.{slugify(config_entry.unique_id)}_alarm_server_{key}"
entity_registry.async_remove(entity_id)
entity = entity_registry.async_get(entity_id)
if entity:
entity_registry.async_remove(entity_id)

hass.config_entries.async_update_entry(
config_entry,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/hikvision_next/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ async def get_schema(self, user_input: dict[str, Any]):
schema = vol.Schema(
{
vol.Required(CONF_HOST, default="http://"): str,
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_SET_ALARM_SERVER, default=True): bool,
vol.Required(CONF_ALARM_SERVER_HOST): str,
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
vol.Optional(RTSP_PORT_FORCED): vol.And(int, vol.Range(min=1)),
}
)
Expand Down
1 change: 1 addition & 0 deletions custom_components/hikvision_next/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ async def _async_get_diagnostics(
"ContentMgmt/Storage",
"Security/adminAccesses",
"Event/triggers",
"Event/channels/capabilities",
"Event/triggers/scenechangedetection-1",
"Event/notification/httpHosts",
"Streaming/channels",
Expand Down
59 changes: 43 additions & 16 deletions custom_components/hikvision_next/isapi/isapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ async def get_hardware_info(self):
await self.get_device_info()
capabilities = (await self.request(GET, "System/capabilities")).get("DeviceCap", {})

self.capabilities.support_analog_cameras = int(deep_get(capabilities, "SysCap.VideoCap.videoInputPortNums", 0))
self.capabilities.support_digital_cameras = int(deep_get(capabilities, "RacmCap.inputProxyNums", 0))
self.capabilities.analog_cameras_inputs = int(deep_get(capabilities, "SysCap.VideoCap.videoInputPortNums", 0))
self.capabilities.digital_cameras_inputs = int(deep_get(capabilities, "RacmCap.inputProxyNums", 0))
self.capabilities.support_holiday_mode = str_to_bool(deep_get(capabilities, "SysCap.isSupportHolidy", "false"))
self.capabilities.support_channel_zero = str_to_bool(
deep_get(capabilities, "RacmCap.isSupportZeroChan", "false")
Expand All @@ -116,7 +116,7 @@ async def get_hardware_info(self):

# Set if NVR based on whether more than 1 supported IP or analog cameras
# Single IP camera will show 0 supported devices in total
if self.capabilities.support_analog_cameras + self.capabilities.support_digital_cameras > 1:
if self.capabilities.analog_cameras_inputs + self.capabilities.digital_cameras_inputs > 1:
self.device_info.is_nvr = True

await self.get_cameras()
Expand All @@ -141,6 +141,7 @@ async def get_cameras(self):
channel_id = int(deep_get(streaming_channel, "Video.videoInputChannelID", 1))
channel_ids.add(channel_id)

self.capabilities.is_multi_channel = len(channel_ids) > 1
for channel_id in sorted(channel_ids):
# Determine camera name
if len(channel_ids) > 1:
Expand All @@ -162,7 +163,7 @@ async def get_cameras(self):
self.cameras.append(camera)
else:
# Get analog and digital cameras attached to NVR
if self.capabilities.support_digital_cameras > 0:
if self.capabilities.digital_cameras_inputs > 0:
digital_cameras = deep_get(
(await self.request(GET, "ContentMgmt/InputProxy/channels")),
"InputProxyChannelList.InputProxyChannel",
Expand All @@ -175,12 +176,10 @@ async def get_cameras(self):
if not source:
continue

# Generate serial number if not provided by camera
# As combination of protocol and IP
serial_no = source.get("serialNumber")

if not serial_no:
serial_no = str(source.get("proxyProtocol")) + str(source.get("ipAddress", "")).replace(".", "")
if not serial_no or self.get_camera_by_serial_no(serial_no):
# serial no is not always recognized correcly by NVR
serial_no = f"{self.device_info.serial_no}_{source.get("proxyProtocol")}_{camera_id}"

self.cameras.append(
IPCamera(
Expand All @@ -198,7 +197,7 @@ async def get_cameras(self):
)

# Get analog cameras
if self.capabilities.support_analog_cameras > 0:
if self.capabilities.analog_cameras_inputs > 0:
analog_cameras = deep_get(
(await self.request(GET, "System/Video/inputs/channels")),
"VideoInputChannelList.VideoInputChannel",
Expand Down Expand Up @@ -240,7 +239,7 @@ async def get_protocols(self):
async def get_supported_events(self, system_capabilities: dict) -> list[EventInfo]:
"""Get list of all supported events available."""

def get_event(event_trigger: dict):
def create_event_info(event_trigger: dict):
notification_list = event_trigger.get("EventTriggerNotificationList", {}) or {}

event_type = event_trigger.get("eventType")
Expand Down Expand Up @@ -285,15 +284,17 @@ def get_event(event_trigger: dict):
)

events = []

# Get events from Event/triggers
event_triggers = await self.request(GET, "Event/triggers")
event_notification = event_triggers.get("EventNotification")
if event_notification:
supported_events = deep_get(event_notification, "EventTriggerList.EventTrigger", [])
available_events = deep_get(event_notification, "EventTriggerList.EventTrigger", [])
else:
supported_events = deep_get(event_triggers, "EventTriggerList.EventTrigger", [])
available_events = deep_get(event_triggers, "EventTriggerList.EventTrigger", [])

for event_trigger in supported_events:
if event := get_event(event_trigger):
for event_trigger in available_events:
if event := create_event_info(event_trigger):
events.append(event)

# some devices do not have scenechangedetection in Event/triggers
Expand All @@ -302,9 +303,28 @@ def get_event(event_trigger: dict):
if is_supported:
event_trigger = await self.request(GET, "Event/triggers/scenechangedetection-1")
event_trigger = deep_get(event_trigger, "EventTrigger", {})
if event := get_event(event_trigger):
if event := create_event_info(event_trigger):
events.append(event)

# multichannel camera needs to fetch events for each channel
if self.capabilities.is_multi_channel:
channels_capabilities = await self.request(GET, "Event/channels/capabilities")
channel_events = deep_get(channels_capabilities, "ChannelEventCapList.ChannelEventCap", [])
for event_cap in channel_events:
event_types = deep_get(event_cap, "eventType").get("@opt", "").split(",")
channel_id = int(event_cap.get("channelID"))
for event_type in event_types:
event_id = event_type.lower()
if event_id in EVENTS_ALTERNATE_ID:
event_id = EVENTS_ALTERNATE_ID[event_id]
if event_id not in EVENTS:
continue
if not [e for e in events if (e.id == event_id and e.channel_id == channel_id)]:
event_trigger = await self.request(GET, f"Event/triggers/{event_id}-{channel_id}")
event_trigger = deep_get(event_trigger, "EventTrigger", {})
if event := create_event_info(event_trigger):
events.append(event)

return events

def get_event_url(self, event_id: str, channel_id: int, io_port_id: int, is_proxy: bool) -> str | None:
Expand Down Expand Up @@ -367,6 +387,13 @@ def get_camera_by_id(self, camera_id: int) -> IPCamera | AnalogCamera | None:
# Camera id does not exist
return None

def get_camera_by_serial_no(self, serial_no: str) -> IPCamera | AnalogCamera | None:
"""Get camera object by serial number."""
for c in self.cameras:
if c.serial_no == serial_no:
return c
return None

async def get_storage_devices(self):
"""Get HDD and NAS storage devices."""
storage_list = []
Expand Down
5 changes: 3 additions & 2 deletions custom_components/hikvision_next/isapi/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ class ISAPIDeviceInfo:
class CapabilitiesInfo:
"""Holds info of an NVR/DVR or single IP Camera."""

support_analog_cameras: int = 0
support_digital_cameras: int = 0
analog_cameras_inputs: int = 0 # number of analog cameras connected to NVR/DVR
digital_cameras_inputs: int = 0 # number of digital cameras connected to NVR/DVR
is_multi_channel: bool = False # if camera has multiple channels
support_holiday_mode: bool = False
support_alarm_server: bool = False
support_channel_zero: bool = False
Expand Down
2 changes: 1 addition & 1 deletion custom_components/hikvision_next/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"xmltodict==0.13.0",
"requests-toolbelt==1.0.0"
],
"version": "1.1.0"
"version": "1.1.1"
}
9 changes: 4 additions & 5 deletions custom_components/hikvision_next/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,9 @@ async def async_setup_entry(
for event in camera.events_info:
entities.append(EventSwitch(camera.id, event, events_coordinator))

# NVR supported events
if device.device_info.is_nvr:
for event in device.events_info:
entities.append(EventSwitch(0, event, events_coordinator))
# Device supported events
for event in device.events_info:
entities.append(EventSwitch(0, event, events_coordinator))

# Output port switch
for i in range(1, device.capabilities.output_ports + 1):
Expand Down Expand Up @@ -117,7 +116,7 @@ def __init__(self, coordinator, port_no: int) -> None:
@property
def is_on(self) -> bool | None:
"""Turn on."""
return self.coordinator.data.get(self.unique_id) == "active"
return self.coordinator.data.get(self.unique_id)

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on."""
Expand Down
4 changes: 2 additions & 2 deletions custom_components/hikvision_next/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"host": "URL",
"password": "Password",
"username": "Username",
"verify_ssl": "Verify Hikvision device SSL certificate (uncheck for self-signed certificate)",
"rtsp_port_forced": "RTSP port forwarded on router (Optional)",
"verify_ssl": "Verify SSL certificate (uncheck for self-signed certificate)",
"rtsp_port_forced": "Force RTSP port (Optional)",
"set_alarm_server": "Set notifications host using following address:",
"alarm_server": "Home Assistant address accessible by Hikvison device"
}
Expand Down
4 changes: 2 additions & 2 deletions custom_components/hikvision_next/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"host": "URL",
"password": "Mot de passe",
"username": "Nom d'utilisateur",
"verify_ssl": "Vérifiez le certificat SSL de l'appareil Hikvision (décochez la case pour un certificat self-signed)",
"rtsp_port_forced": "Port RTSP transféré sur le routeur (facultatif)",
"verify_ssl": "Vérifiez le certificat SSL (décochez la case pour un certificat self-signed)",
"rtsp_port_forced": "Forcer le port RTSP (facultatif)",
"set_alarm_server": "Définir le serveur d'alarme à l'aide l'adresse suivante :",
"alarm_server": "Adresse d'Home Assistant accessible par l'appareil Hikvision"
}
Expand Down
4 changes: 2 additions & 2 deletions custom_components/hikvision_next/translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"host": "URL",
"password": "Password",
"username": "Username",
"verify_ssl": "Verificare il certificato SSL del dispositivo Hikvision (deselezionare per il certificato self-signed)",
"rtsp_port_forced": "Porta RTSP inoltrata sul router (facoltativo)",
"verify_ssl": "Verificare il certificato SSL (deselezionare per il certificato self-signed)",
"rtsp_port_forced": "Forza la porta RTSP (facoltativo)",
"set_alarm_server": "Indirizzo del server di ricezione degli allarmi:",
"alarm_server": "indirizzo di Home Assistant accessibile dal dispositivo Hikvison"
}
Expand Down
4 changes: 2 additions & 2 deletions custom_components/hikvision_next/translations/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"host": "URL",
"password": "Hasło",
"username": "Nazwa użytkownika",
"rtsp_port_forced": "Port RTSP przekierowany na routerze (opcjonalnie)",
"verify_ssl": "Zweryfikuj certyfikat SSL urządzenia Hikvision (odznacz dla certyfikatu self-signed)",
"rtsp_port_forced": "Wymuś port RTSP (opcjonalnie)",
"verify_ssl": "Zweryfikuj certyfikat SSL (odznacz dla certyfikatu self-signed)",
"set_alarm_server": "Ustaw host powiadomień używając następującego adresu:",
"alarm_server": "Adres Home Assistant dostępny dla urządzenia Hikvision"
}
Expand Down
4 changes: 2 additions & 2 deletions custom_components/hikvision_next/translations/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"host": "URL",
"password": "Senha",
"username": "Nome de usuário",
"rtsp_port_forced": "Porta RTSP encaminhada no roteador (opcional)",
"verify_ssl": "Verifique o certificado SSL do dispositivo Hikvision (desmarque o certificado self-signed)",
"rtsp_port_forced": "Forçar porta RTSP (opcional)",
"verify_ssl": "Verifique o certificado SSL (desmarque o certificado self-signed)",
"set_alarm_server": "Defina o servidor de alarme usando o seguinte endereço:",
"alarm_server": "Endereço do Home Assistant acessível pelo dispositivo Hikvison"
}
Expand Down
4 changes: 2 additions & 2 deletions custom_components/hikvision_next/translations/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"host": "Link",
"password": "Password",
"username": "Utilizador",
"rtsp_port_forced": "Porta RTSP encaminhada no router (opcional)",
"verify_ssl": "Verifique o certificado SSL do dispositivo Hikvision (desmarque o certificado self-signed)",
"rtsp_port_forced": "Forçar porta RTSP (opcional)",
"verify_ssl": "Verifique o certificado SSL (desmarque o certificado self-signed)",
"set_alarm_server": "Sefinir um alarme usando o seguinte endereço:",
"alarm_server": "Home Assistant endreço é acessivel pelo Hikvison device?"
}
Expand Down
4 changes: 2 additions & 2 deletions custom_components/hikvision_next/translations/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"host": "URL",
"password": "Пароль",
"username": "Имя пользователя",
"rtsp_port_forced": "Порт RTSP перенаправлен на маршрутизатор (необязательно)",
"verify_ssl": "Проверьте SSL-сертификат устройства Hikvision (снимите флажок для self-signed сертификата)",
"rtsp_port_forced": "Принудительный RTSP-порт (необязательно)",
"verify_ssl": "Проверьте SSL-сертификат (снимите флажок для self-signed сертификата)",
"set_alarm_server": "Установить сервер тревоги используя следующий адрес:",
"alarm_server": "Home Assistant адрес, доступный Hikvison устройству"
}
Expand Down
Loading

0 comments on commit 199c19a

Please sign in to comment.