Skip to content

Commit

Permalink
release/v1.0.19 (#229)
Browse files Browse the repository at this point in the history
* improved pir support detection #220 (#222)

* support for renaming entity id (#224)

* added reboot service (#225)

* Feature/isapi request service (#227)

* blueprint for sensor state on video

* Fix/doorbell initialization (#228)

* changed isapi auth checking request

* version bump v1.0.19
  • Loading branch information
maciej-or authored Oct 26, 2024
1 parent 9250e4f commit b42f5ad
Show file tree
Hide file tree
Showing 19 changed files with 1,569 additions and 34 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The Home Assistant integration for Hikvision NVRs and IP cameras. Receives and s
- Image entities for the latest snapshots
- Tracking HDD and NAS status
- Tracking Notifications Host settings for diagnostic purposes
- Remote reboot device
- Basic and digest authentication support

### Supported events
Expand All @@ -34,9 +35,18 @@ Events must be set to alert the surveillance center in Linkage Action for Home A

### Blueprints

Take Multiple Snapshots On Detection Event
#### Take Multiple Snapshots On Detection Event

Creates automation that allows to take snapshots from selected cameras when an event sensor is triggered.

[<img src="https://my.home-assistant.io/badges/blueprint_import.svg">](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)

#### Display Sensor State On Hikvision Video

Creates an automation that allows to display text overlay on a selected video stream with the state of a selected sensor. Refreshes every 15 minutes.

[<img src="https://my.home-assistant.io/badges/blueprint_import.svg">](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https://github.com/maciej-or/hikvision_next/blob/main/blueprints/display_sensor_state_on_hikvision_video.yaml)

## Preview

### IP Camera device view
Expand Down
81 changes: 81 additions & 0 deletions blueprints/display_sensor_state_on_hikvision_video.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
blueprint:
name: Display Sensor State On Hikvision Video
description: |
Sets an overlay text on a Hikvision camera video.
domain: automation
input:
config_entry_id:
name: "Device"
description: "The Hikvision device."
selector:
config_entry:
integration: hikvision_next
camera_no:
name: "Camera"
description: "The camera number."
default: 1
selector:
number:
min: 1
max: 32
step: 1
mode: box
sensor_entity:
name: Sensor
description: "The sensor entity to display on the video."
selector:
entity:
domain: sensor
position_x:
name: "X"
description: "The X position of the overlay text."
default: 16
selector:
number:
min: 0
max: 1920
step: 1
position_y:
name: "Y"
description: "The Y position of the overlay text."
default: 570
selector:
number:
min: 0
max: 1080
step: 1
enabled:
name: "State"
description: "Enable or disable the overlay text."
default: true
selector:
boolean: {}
mode: single
variables:
entity_id: !input sensor_entity
camera_no: !input camera_no
position_x: !input position_x
position_y: !input position_y
enabled: !input enabled
trigger:
- platform: time_pattern
minutes: /15
action:
- service: hikvision_next.isapi_request
data:
method: PUT
config_entry_id: !input config_entry_id
path: "/System/Video/inputs/channels/{{camera_no}}/overlays/text"
payload: >
<?xml version="1.0" encoding="UTF-8"?>
<TextOverlayList version="2.0" xmlns="http://www.hikvision.com/ver20/XMLSchema">
<TextOverlay>
<id>1</id>
<enabled>{{ enabled | lower }}</enabled>
<positionX>{{ position_x }}</positionX>
<positionY>{{ position_y }}</positionY>
<displayText>{{ states(entity_id, with_unit=True) }}</displayText>
<directAngle></directAngle>
</TextOverlay>
</TextOverlayList>
response_variable: response
19 changes: 15 additions & 4 deletions custom_components/hikvision_next/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import asyncio
from contextlib import suppress
import logging
import traceback

from httpx import TimeoutException

Expand All @@ -18,6 +19,7 @@
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.typing import ConfigType

from .const import (
ALARM_SERVER_PATH,
Expand All @@ -31,6 +33,7 @@
from .coordinator import EventsCoordinator, SecondaryCoordinator
from .isapi import ISAPI
from .notifications import EventNotificationsView
from .services import setup_services

_LOGGER = logging.getLogger(__name__)

Expand All @@ -43,6 +46,14 @@
]


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Hikvision component."""

setup_services(hass)

return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up integration from a config entry."""
hass.data.setdefault(DOMAIN, {})
Expand All @@ -59,12 +70,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device_info = isapi.hass_device_info()
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(config_entry_id=entry.entry_id, **device_info)
except (asyncio.TimeoutError, TimeoutException) as ex:
except (TimeoutError, TimeoutException) as ex:
raise ConfigEntryNotReady(f"Timeout while connecting to {host}. Cannot initialize {DOMAIN}") from ex
except Exception as ex: # pylint: disable=broad-except
raise ConfigEntryNotReady(
f"Unknown error connecting to {host}. Cannot initialize {DOMAIN}. Error is {ex}"
) from ex
msg = f"Cannot initialize {DOMAIN} {host}. Error: {ex}\n"
_LOGGER.error(msg + traceback.format_exc())
raise ConfigEntryNotReady(msg) from ex

coordinators = {}

Expand Down
4 changes: 3 additions & 1 deletion custom_components/hikvision_next/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.httpx_client import get_async_client

from .const import DATA_ALARM_SERVER_HOST, DATA_SET_ALARM_SERVER, DOMAIN
from .isapi import ISAPI
Expand Down Expand Up @@ -62,7 +63,8 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Flo
CONF_HOST: host,
}

isapi = ISAPI(host, username, password)
session = get_async_client(self.hass)
isapi = ISAPI(host, username, password, session)
await isapi.get_device_info()

if self._reauth_entry:
Expand Down
5 changes: 5 additions & 0 deletions custom_components/hikvision_next/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
CONNECTION_TYPE_DIRECT = "Direct"
CONNECTION_TYPE_PROXIED = "Proxied"

ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ACTION_REBOOT = "reboot"
ACTION_ISAPI_REQUEST = "isapi_request"
ACTION_UPDATE_SNAPSHOT = "update_snapshot"

HIKVISION_EVENT = f"{DOMAIN}_event"
EVENT_BASIC: Final = "basic"
EVENT_IO: Final = "io"
Expand Down
12 changes: 6 additions & 6 deletions custom_components/hikvision_next/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ async def _async_update_data(self):
if event.disabled:
continue
try:
entity_id = ENTITY_ID_FORMAT.format(event.unique_id)
data[entity_id] = await self.isapi.get_event_enabled_state(event)
_id = ENTITY_ID_FORMAT.format(event.unique_id)
data[_id] = await self.isapi.get_event_enabled_state(event)
except Exception as ex: # pylint: disable=broad-except
self.isapi.handle_exception(ex, f"Cannot fetch state for {event.id}")

Expand All @@ -55,18 +55,18 @@ async def _async_update_data(self):
if event.disabled:
continue
try:
entity_id = ENTITY_ID_FORMAT.format(event.unique_id)
data[entity_id] = await self.isapi.get_event_enabled_state(event)
_id = ENTITY_ID_FORMAT.format(event.unique_id)
data[_id] = await self.isapi.get_event_enabled_state(event)
except Exception as ex: # pylint: disable=broad-except
self.isapi.handle_exception(ex, f"Cannot fetch state for {event.id}")

# Get output port(s) status
for i in range(1, self.isapi.device_info.output_ports + 1):
try:
entity_id = ENTITY_ID_FORMAT.format(
_id = ENTITY_ID_FORMAT.format(
f"{slugify(self.isapi.device_info.serial_no.lower())}_{i}_alarm_output"
)
data[entity_id] = await self.isapi.get_port_status("output", i)
data[_id] = await self.isapi.get_port_status("output", i)
except Exception as ex: # pylint: disable=broad-except
self.isapi.handle_exception(ex, f"Cannot fetch state for alarm output {i}")

Expand Down
4 changes: 2 additions & 2 deletions custom_components/hikvision_next/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from homeassistant.helpers.template import Template
from homeassistant.util import slugify

from .const import DATA_ISAPI, DOMAIN
from .const import ACTION_UPDATE_SNAPSHOT, DATA_ISAPI, DOMAIN
from .isapi import ISAPI, CameraStreamInfo

_LOGGER = logging.getLogger(__name__)
Expand All @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e

platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
"update_snapshot",
ACTION_UPDATE_SNAPSHOT,
{vol.Required(CONF_FILENAME): cv.template},
"update_snapshot_filename",
)
Expand Down
15 changes: 13 additions & 2 deletions custom_components/hikvision_next/isapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,19 @@
import datetime
from functools import reduce
from http import HTTPStatus
import httpx
import json
import logging
from typing import Any, Optional
from urllib.parse import quote, urlparse

import httpx
from httpx import HTTPStatusError, TimeoutException
import xmltodict

from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.util import slugify
from .isapi_client import ISAPI_Client

from .const import (
CONNECTION_TYPE_DIRECT,
Expand All @@ -35,6 +34,7 @@
MUTEX_ALTERNATE_IDS,
STREAM_TYPE,
)
from .isapi_client import ISAPI_Client

Node = dict[str, Any]

Expand Down Expand Up @@ -410,6 +410,11 @@ def get_event(event_trigger: dict):
if not event_type:
return None

if event_type.lower() == EVENT_PIR:
is_supported = str_to_bool(deep_get(system_capabilities, "WLAlarmCap.isSupportPIR", False))
if not is_supported:
return None

channel = event_trigger.get("videoInputChannelID", event_trigger.get("dynVideoInputChannelID", 0))
io_port = event_trigger.get("inputIOPortID", event_trigger.get("dynInputIOPortID", 0))
notifications = notification_list.get("EventTriggerNotification", [])
Expand All @@ -435,6 +440,8 @@ def get_event(event_trigger: dict):
supported_events = deep_get(event_notification, "EventTriggerList.EventTrigger", [])
else:
supported_events = deep_get(event_triggers, "EventTriggerList.EventTrigger", [])
if not isinstance(supported_events, list):
supported_events = [supported_events]

for event_trigger in supported_events:
if event := get_event(event_trigger):
Expand Down Expand Up @@ -772,6 +779,10 @@ async def set_alarm_server(self, base_url: str, path: str) -> None:
xml = xmltodict.unparse(data)
await self.request(PUT, "Event/notification/httpHosts", present="xml", data=xml)

async def reboot(self):
"""Reboot device."""
await self.request(PUT, "System/reboot", present="xml")

async def request(
self,
method: str,
Expand Down
24 changes: 16 additions & 8 deletions custom_components/hikvision_next/isapi_client.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import httpx
import logging
from typing import Any, AsyncIterator, List, Union
from urllib.parse import urljoin
import json
import xmltodict
from dataclasses import dataclass

_LOGGER = logging.getLogger(__name__)


def response_parser(response, present="dict"):
"""Parse Hikvision results."""
Expand Down Expand Up @@ -42,16 +45,21 @@ async def _detect_auth_method(self):
if not self.session:
self.session = httpx.AsyncClient(timeout=self.timeout)

url = urljoin(self.host, self.isapi_prefix + "/System/status")
for method in [
httpx.BasicAuth(self.username, self.password),
httpx.DigestAuth(self.username, self.password),
]:
response = await self.session.get(url, auth=method)
if response.status_code == 200:
self._auth_method = method
url = urljoin(self.host, self.isapi_prefix + "/System/deviceInfo")
_LOGGER.debug("--- [WWW-Authenticate detection] %s", self.host)
response = await self.session.get(url)
if response.status_code == 401:
www_authenticate = response.headers.get("WWW-Authenticate", "")
_LOGGER.debug("WWW-Authenticate header: %s", www_authenticate)
if "Basic" in www_authenticate:
self._auth_method = httpx.BasicAuth(self.username, self.password)
elif "Digest" in www_authenticate:
self._auth_method = httpx.DigestAuth(self.username, self.password)

if not self._auth_method:
_LOGGER.error("Authentication method not detected, %s", response.status_code)
if response.headers:
_LOGGER.error("response.headers %s", response.headers)
response.raise_for_status()

def get_url(self, relative_url: str) -> str:
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.0.18"
"version": "1.0.19"
}
Loading

0 comments on commit b42f5ad

Please sign in to comment.