hikvision_next icon indicating copy to clipboard operation
hikvision_next copied to clipboard

HA Received the Alarm Server Notification but no binary_sensor is seen

Open GeoCodeTout opened this issue 1 year ago • 17 comments

Hello,

I have a HIK camera - DS-2CD1643G2-LIZSU.

I have enabled motion detection. The camera has the capability to detect whether it's a vehicle or a person.

I correctly receive the motion notification from the camera but an exception is raised by Home Assistant.

In the Home Assistant log, here is what I have:

2025-01-14 23:12:25.208 DEBUG (MainThread) [custom_components.hikvision_next.notifications] --- Incoming event notification ---
2025-01-14 23:12:25.208 DEBUG (MainThread) [custom_components.hikvision_next.notifications] Source: 192.169.1.30
2025-01-14 23:12:25.208 DEBUG (MainThread) [custom_components.hikvision_next.notifications] request headers: <CIMultiDictProxy('Content-Type': 'multipart/form-data; boundary=boundary', 'Host': '192.169.1.1:8123', 'Connection': 'close', 'Content-Length': '907')>
2025-01-14 23:12:25.209 DEBUG (MainThread) [custom_components.hikvision_next.notifications] part headers: {'Content-Disposition': 'form-data; name="MoveDetection.xml"; filename="MoveDetection.xml"', 'Content-Type': 'application/xml', 'Content-Length': '737'}
2025-01-14 23:12:25.209 DEBUG (MainThread) [custom_components.hikvision_next.notifications] alert info: <?xml version="1.0" encoding="UTF-8"?>
<EventNotificationAlert version="2.0" xmlns="http://www.hikvision.com/ver20/XMLSchema">
<ipAddress>192.169.1.30</ipAddress>
<portNo>8123</portNo>
<protocol>HTTP</protocol>
<macAddress>5c:34:5b:e4:de:14</macAddress>
<channelID>1</channelID>
<dateTime>2025-01-14T23:12:25+01:00</dateTime>
<activePostCount>1</activePostCount>
<eventType>VMD</eventType>
<eventState>active</eventState>
<eventDescription>Motion alarm</eventDescription>
<channelName>Camera 01</channelName>
<targetType>human</targetType>
<targetInfo>
<targetID>1</targetID>
<targetRect>
<X>0.245</X>
<Y>0.018</Y>
<width>0.268</width>
<height>0.583</height>
</targetRect>
</targetInfo>
</EventNotificationAlert>
2025-01-14 23:12:25.210 DEBUG (MainThread) [custom_components.hikvision_next.notifications] **Alert: AlertInfo(channel_id=1, io_port_id=0, event_id='motiondetection', device_serial_no=None, mac='5c:34:5b:e4:de:14', region_id=0, detection_target=None)
2025-01-14 23:12:25.210 DEBUG (MainThread) [custom_components.hikvision_next.notifications] UNIQUE_ID: binary_sensor.ds_2cd1643g2_lizsu20240906aawrfn2343166_1_motiondetection**
2025-01-14 23:12:25.210 WARNING (MainThread) [custom_components.hikvision_next.notifications] Cannot process incoming event Entity not found None
2025-01-14 23:13:02.612 DEBUG (MainThread) [custom_components.hikvision_next.isapi.isapi] --- [GET] http://192.169.1.30/ISAPI/System/IO/outputs/1/status
2025-01-14 23:13:02.612 DEBUG (MainThread) [custom_components.hikvision_next.isapi.isapi] 
{'IOPortStatus': {'@version': '2.0', '@xmlns': 'http://www.hikvision.com/ver20/XMLSchema', 'ioPortID': '1', 'ioPortType': 'output', 'ioState': 'inactive'}}
2025-01-14 23:13:02.617 DEBUG (MainThread) [custom_components.hikvision_next.isapi.isapi] --- [GET] http://192.169.1.30/ISAPI/ContentMgmt/Storage
2025-01-14 23:13:02.617 DEBUG (MainThread) [custom_components.hikvision_next.isapi.isapi] 
{'storage': {'@version': '2.0', '@xmlns': 'http://www.hikvision.com/ver20/XMLSchema', 'hddList': {'@version': '1.0', '@xmlns': 'http://www.hikvision.com/ver10/XMLSchema', '@size': '8'}, 'workMode': 'quota'}}
2025-01-14 23:13:02.617 DEBUG (MainThread) [custom_components.hikvision_next.coordinator] Finished fetching hikvision_next data in 0.018 seconds (success: True)

As we can see, it is trying to update the sensor binary_sensor.ds_2cd1643g2_lizsu20240906aawrfn2343166_1_motiondetection.

This sensor is not recognized by the system. In fact, I don’t have any sensors from HIK that are being detected.

Is there any way to update the code in order to create the sensor ?

Sincerely,

GeoCodeTout avatar Jan 15 '25 14:01 GeoCodeTout

Make sure if you have enabled Notify Surveillance Center https://github.com/maciej-or/hikvision_next/issues/257 please send diagnostic data if the problem persists https://github.com/maciej-or/hikvision_next/wiki/How-to-get-Diagnostic-Data

maciej-or avatar Jan 15 '25 20:01 maciej-or

Thank you for your answer.

Notification Center is activated as HA get the notifications.

The problem is on HA which does not generate the Motion sensor.

GeoCodeTout avatar Jan 15 '25 20:01 GeoCodeTout

please send diagnostic data https://github.com/maciej-or/hikvision_next/wiki/How-to-get-Diagnostic-Data

maciej-or avatar Jan 15 '25 21:01 maciej-or

Hello @maciej-or ,

Thank you for your answer. Please find attached the diagnostic data. config_entry-hikvision_next-01JHKB62HJR4WXYJ5D3XB0GV0P.json

GeoCodeTout avatar Jan 15 '25 21:01 GeoCodeTout

If it can help, as you see no sensor for movement has been detected.

Image

Image

When HA gets the camera event : AlertInfo(channel_id=1, io_port_id=0, event_id='motiondetection', device_serial_no=None, mac='5c:34:5b:e4:de:14', region_id=0, detection_target=None

It tries to update the sensor : UNIQUE_ID: binary_sensor.ds_2cd1643g2_lizsu20240906aawrfn2343166_1_motiondetection which is not known by HA system.

GeoCodeTout avatar Jan 17 '25 15:01 GeoCodeTout

I made some searches in your code.

In ISAPI setup, two requests gets an error :

"Event/triggers": { "status_code": 500 },

and

"Event/triggers/scenechangedetection-1": { "status_code": 403 },

In the log:

2025-01-17 17:18:22.741 INFO (MainThread) [custom_components.hikvision_next.isapi.isapi] --- [GET] http://192.169.1.30/ISAPI/Event/triggers/scenechangedetection-1 Client error '403 Forbidden' for url 'http://192.169.1.30/ISAPI/Event/triggers/scenechangedetection-1'

GeoCodeTout avatar Jan 17 '25 16:01 GeoCodeTout

I made some search via ISAPI commands:

/ISAPI/Event/capabilities

gives: <EventCap xmlns="http://www.hikvision.com/ver20/XMLSchema" version="2.0"> <isSupportHDFull>false</isSupportHDFull> <isSupportHDError>true</isSupportHDError> <isSupportNicBroken>true</isSupportNicBroken> <isSupportIpConflict>true</isSupportIpConflict> <isSupportIllAccess>true</isSupportIllAccess> <isSupportViException>false</isSupportViException> <isSupportViMismatch>false</isSupportViMismatch> <isSupportRecordException>false</isSupportRecordException> <isSupportTriggerFocus>false</isSupportTriggerFocus> <isSupportMotionDetection>true</isSupportMotionDetection> <isSupportVideoLoss>false</isSupportVideoLoss> <isSupportTamperDetection>true</isSupportTamperDetection> </EventCap>

so <isSupportMotionDetection>true</isSupportMotionDetection>

I Get configuration capability of alarm linkage action Request URL: GET /ISAPI/Event/triggersCap

it returns: <EventTriggersCap xmlns="http://www.hikvision.com/ver20/XMLSchema" version="2.0"> <DiskerrorTriggerCap> <isSupportCenter>true</isSupportCenter> <isSupportIO>true</isSupportIO> <isSupportEmail>true</isSupportEmail> </DiskerrorTriggerCap> <StorageDetectionTriggerCap> <isSupportCenter>true</isSupportCenter> <isSupportIO>true</isSupportIO> <isSupportEmail>true</isSupportEmail> </StorageDetectionTriggerCap> <NicbrokenTriggerCap> <isSupportIO>true</isSupportIO> </NicbrokenTriggerCap> <IpconflictTriggerCap> <isSupportIO>true</isSupportIO> </IpconflictTriggerCap> <IllaccesTriggerCap> <isSupportCenter>true</isSupportCenter> <isSupportIO>true</isSupportIO> <isSupportEmail>true</isSupportEmail> </IllaccesTriggerCap> <IOTriggerCap> <isSupportCenter>true</isSupportCenter> <isSupportRecord>true</isSupportRecord> <isSupportBeep>true</isSupportBeep> <isSupportIO>true</isSupportIO> <isSupportFTP>true</isSupportFTP> <isSupportEmail>true</isSupportEmail> </IOTriggerCap> <MotionDetectionTriggerCap> <isSupportCenter>true</isSupportCenter> <isSupportRecord>true</isSupportRecord> <isSupportBeep>true</isSupportBeep> <isSupportIO>true</isSupportIO> <isSupportFTP>true</isSupportFTP> <isSupportEmail>true</isSupportEmail> </MotionDetectionTriggerCap> <TamperDetectionTriggerCap> <isSupportCenter>true</isSupportCenter> <isSupportIO>true</isSupportIO> <isSupportEmail>true</isSupportEmail> </TamperDetectionTriggerCap> <isSupportAudioAction>true</isSupportAudioAction> <isSupportAudioTrigger>true</isSupportAudioTrigger> </EventTriggersCap>

GeoCodeTout avatar Jan 17 '25 16:01 GeoCodeTout

And I have a strange behavior on

ISAPI Event/triggers

It returns <ResponseStatus xmlns="http://www.hikvision.com/ver20/XMLSchema" version="2.0"> <requestURL/> <statusCode>3</statusCode> <statusString>Device Error</statusString> <subStatusCode>deviceError</subStatusCode> <description> deviceError. isapi_get_event_triggerinfo error: diskfull. </description> </ResponseStatus>

Why deviceError. isapi_get_event_triggerinfo error: diskfull. ?

I try to remove my Sd Card --> idem. I Format it --> idem

GeoCodeTout avatar Jan 17 '25 16:01 GeoCodeTout

I think that the error in ISAPI scan is in these lines:

# Get events from Event/triggers
        event_triggers = await self.request(GET, "Event/triggers")
        event_notification = event_triggers.get("EventNotification")
        if event_notification:
            available_events = deep_get(event_notification, "EventTriggerList.EventTrigger", [])
        else:
            available_events = deep_get(event_triggers, "EventTriggerList.EventTrigger", [])
        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
        if not [e for e in events if e.id == "scenechangedetection"]:
            is_supported = str_to_bool(deep_get(system_capabilities, "SmartCap.isSupportSceneChangeDetection", False))
            if is_supported:
                event_trigger = await self.request(GET, "Event/triggers/scenechangedetection-1")
                event_trigger = deep_get(event_trigger, "EventTrigger", {})
                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

I know that

1° Event/triggers --> Return error (see previous post) 2° scenechangedetection is not know by my ISAPI request on the camera 3° if self.capabilities.is_multi_channel: I don't think that the camera is multichannel... but if a do GET Event/channels/capabilities

I get this:

<ChannelEventCapList xmlns="http://www.hikvision.com/ver20/XMLSchema" version="2.0"> <ChannelEventCap> <eventType opt="motionDetection,VMD,tamperDetection,Shelteralarm,storageDetection,diskerror,nicbroken,ipconflict,illaccess,IO"/> <shieldEventType opt=""/> <channelID>1</channelID> </ChannelEventCap> </ChannelEventCapList>

...

so maybe that my camera is not multichannel but your code need to go in to load the <channelID>1</channelID> in all case.

GeoCodeTout avatar Jan 17 '25 17:01 GeoCodeTout

If you want to access to camera to make tests.

82.64.108.28 Port 6060

user: maciejor password: hikvision2025 (you have to change it at the first connection)

Nothing sensible, you can connect. Will be reset after.

GeoCodeTout avatar Jan 17 '25 18:01 GeoCodeTout

Thanks, direct access explained a lot. Indeed "Event/triggers" should provide information about all events supported by the cam. Apparently Hikvision enginers decided to break backward compatibility and get any event details in separate requests like /ISAPI/Event/triggers/VMD-1 for motion detection and /ISAPI/Event/triggers/tamper-1 Will grab details tmw and add support for that. I'm afraid more devices will work this way.

maciej-or avatar Jan 17 '25 19:01 maciej-or

I couldn't find any mention of this update in firmware changelogs, but they likely implemented it without any announcements. I read that they've released a new SDK, so they might migrate to that as well.

You'll be more effective at fixing the bug. I tried editing isapi.py to directly create the sensor from /ISAPI/Event/triggers/VMD, but since I'm not familiar with your code, I couldn't get it to work in a few tentatives.

Saw that Event/triggers/VMD works (without the channel ID).... It may be more robust for multichannel models (iterate on channels ?) Maybe the good way is to get Event/channels/capabilities and iterate with Event/triggers/EVENT_TYPE

with EVENT_TYPE : motionDetection,VMD,tamperDetection,Shelteralarm,storageDetection,diskerror,nicbroken,ipconflict (from Event/channels/capabilities)

EventNotification changed to EventTriggerNotification and EventTriggerList to EventTriggerNotificationList

I am sure that a new step for this kind of sensor configuration should not be so hard. Maybe just detect the 500 error and switching the case...

I tried few minutes something like that:

# LUCKY TRY
      event_triggers = await self.request(GET, "Event/triggers/VMD")
      event_notification = event_triggers.get("EventTriggerNotification")
      if event_notification:
          available_events = deep_get(event_notification, "EventTriggerNotificationList.EventTriggerNotification", [])
      else:
          available_events = deep_get(event_triggers, "EventTriggerNotificationList.EventTriggerNotification", [])

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

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

      for event_trigger in available_events:
          if event := create_event_info(event_trigger):
              events.append(event)
      ### END OF LUCKY TRY

Unlucky ! I have to dig into your structure to get it working. You should get it fast... If you need some help. I can looking deeply in deep_get and create_event_info which are still black box for me.

I'm sure you already have this, but it might be useful for anyone looking to explore the ISAPI protocol for HIK. Attached is the latest version of the API I found (note that Event/triggers isn't documented in this one).

Thanks a lot!

isapi.pdf

GeoCodeTout avatar Jan 17 '25 21:01 GeoCodeTout

Your insights are right, approach for multichannel cams could work. BTW this PDF is from 2019. I also have ones from 2020 and 2022. I wonder what the latest versions look like. Anyway I'm going to add this way to get supported events.

maciej-or avatar Jan 18 '25 19:01 maciej-or

`async def parse_event_request(self, request: web.Request) -> str: """ Извлекает XML-содержимое из входящего запроса, который может быть обычным текстовым или multipart/form-data.

- Если запрос содержит XML напрямую (например, Content-Type: application/xml), то данные декодируются как XML.
- Если Content-Type равен multipart/form-data, то перебираются все части:
    - Сначала ищется часть, где Content-Type содержит 'xml', и используется её содержимое.
    - Если такой части нет, ищется часть с 'json', затем пытаемся преобразовать JSON в XML.
- Если ни XML, ни JSON не найдены, выбрасывается исключение.
"""
# Считываем все данные из запроса
data = await request.read()

# Получаем заголовок Content-Type из запроса
content_type_header = request.headers.get(CONTENT_TYPE)
if not content_type_header:
    raise ValueError("Missing Content-Type header")
# Приводим заголовок к нижнему регистру и убираем лишние пробелы для унификации
content_type_header = content_type_header.strip().lower()

_LOGGER.debug("Request headers: %s", request.headers)
xml = None  # Здесь будет храниться извлечённое XML-содержимое

# Если Content-Type содержит 'xml', значит данные уже в виде XML
if "xml" in content_type_header:
    try:
        # Декодируем данные в строку, используя UTF-8 (при необходимости ошибки заменяются)
        xml = data.decode("utf-8", errors="replace")
        _LOGGER.debug("Direct XML decoded: %s", xml)
    except Exception as e:
        _LOGGER.error("Error decoding XML: %s", e)
        raise ValueError("Error decoding XML") from e

# Если Content-Type указывает на multipart/form-data, нужно разобрать каждую часть запроса
elif "multipart/form-data" in content_type_header:
    try:
        # Инициализируем декодер для multipart-данных
        decoder = MultipartDecoder(data, content_type_header)
    except Exception as e:
        _LOGGER.error("Error decoding multipart/form-data: %s", e)
        raise ValueError(f"Error decoding multipart/form-data: {content_type_header}") from e

    # Перебираем все части запроса, пытаясь найти ту, что содержит XML
    for part in decoder.parts:
        # Получаем заголовок Content-Type для данной части и приводим его к нижнему регистру
        part_ct = part.headers.get(b"Content-Type", b"").decode("ascii", errors="replace").lower()
        _LOGGER.debug("Multipart part Content-Type: %s", part_ct)
        if "xml" in part_ct:
            try:
                # Если в части найден XML, декодируем её текст
                xml = part.text
                _LOGGER.debug("XML part found: %s", xml)
            except Exception as e:
                _LOGGER.error("Error decoding XML part: %s", e)
                continue  # Если не удалось декодировать, переходим к следующей части
            break  # XML найден, можно завершить перебор частей

    # Если XML не найден, пытаемся найти часть с JSON и преобразовать её в XML
    if not xml:
        for part in decoder.parts:
            part_ct = part.headers.get(b"Content-Type", b"").decode("ascii", errors="replace").lower()
            if "json" in part_ct:
                try:
                    json_text = part.text
                    _LOGGER.debug("JSON part found: %s", json_text)
                    import json
                    # Пытаемся загрузить текст как JSON-объект
                    json_data = json.loads(json_text)
                    # Преобразуем JSON в XML с использованием вспомогательной функции
                    xml = json_to_xml(json_data, root_tag="AlertInfo")
                    _LOGGER.debug("Converted JSON to XML: %s", xml)
                except Exception as e:
                    _LOGGER.error("Error converting JSON to XML: %s", e)
                    # Если преобразование не удалось, используем исходный JSON как запасной вариант
                    xml = json_text
                break
else:
    # Если Content-Type не соответствует ни XML, ни multipart, выбрасываем исключение
    raise ValueError(f"Unexpected event Content-Type {content_type_header}")

# Если после всех попыток xml так и не был извлечён, выбрасываем исключение с описанием ошибки
if not xml:
    raise ValueError(f"Could not extract XML from event with Content-Type {content_type_header}")

return xml

def json_to_xml(json_obj: dict, root_tag: str = "root") -> str: """ Преобразует объект JSON (словарь) в строку XML с указанным корневым тегом.

Использует рекурсивную функцию build_element для обхода вложенных структур:
- Если данные являются словарем, для каждого ключа создаётся подэлемент.
- Если данные являются списком, для каждого элемента списка создаётся тег 'item'.
- В противном случае данные преобразуются в строку и устанавливаются как текст элемента.
"""
import xml.etree.ElementTree as ET

def build_element(elem: ET.Element, data: Any):
    # Если данные представляют собой словарь, обрабатываем каждый ключ-значение
    if isinstance(data, dict):
        for key, val in data.items():
            # Создаем подэлемент, нормализуя имя тега (удаляем пробелы)
            child = ET.SubElement(elem, key.strip().replace(" ", "_"))
            build_element(child, val)
    # Если данные представляют собой список, создаем элемент "item" для каждого элемента списка
    elif isinstance(data, list):
        for item in data:
            item_elem = ET.SubElement(elem, "item")
            build_element(item_elem, item)
    else:
        # Если данные – простой тип (число, строка и т.д.), устанавливаем его как текст элемента
        elem.text = str(data)

# Создаем корневой элемент XML с заданным тегом
root = ET.Element(root_tag)
# Рекурсивно строим дерево XML из JSON-объекта
build_element(root, json_obj)
# Преобразуем XML-дерево в строку (без байтовых данных)
xml_string = ET.tostring(root, encoding="unicode")
return xml_string

` С такой ошибкой я сталкиваюсь, выше решение от ИИ, не знаю как внедрить Cannot process incoming event Unexpected event Content-Type multipart/form-data; boundary=boundary

apande avatar Feb 19 '25 23:02 apande

Hello

Can you get this in english ? I am not sure that your problem is relative to the discussion above.

By the way @maciej-or do you plan to provide a new delivery with the update ? (Sorry to ask, I know that we all have a lot to do).

Otherwise I will try to dive in your code.

GeoCodeTout avatar Feb 19 '25 23:02 GeoCodeTout

@apande if you use AI to generate code let it to translate comments and the whole post, we speak english here if you haven't noticed, furthermore your post is not related to the topic of the thread

@GeoCodeTout feel free to contribute, if you send a PR I will review the code and help if needed. I shared you the ISAPI docs.

maciej-or avatar Feb 20 '25 18:02 maciej-or

Have the same issue on my fresh DS-2CD2347G3-LIY camera. Firmware Version: V5.8.13 build 250725

The fix is extra simple here in isapi/isapi.py:

# multichannel camera needs to fetch events for each channel
if self.capabilities.is_multi_channel or not event_triggers:

And now I have 5 new controls and some triggers: Image

Can use at least fielddetection / Intrusion (Field Detection) now for motion detection, but unfortunately /ISAPI/Event/triggers/motiondetection-1 or /ISAPI/Event/triggers/motiondetection call returns

Client error '403 Forbidden' for url 'http://192.168.1.64/ISAPI/Event/triggers/motiondetection-1'

although motionDetection is supported by my camera and "Motion alarm" with targetType=human is sent.

DS-2CD2347G3-LIY.json

Log file with alerts after the fix above + "dev" branch changes: home-assistant_2025-09-17T03-53-08.079Z.log

hammer-dp-ua avatar Sep 17 '25 04:09 hammer-dp-ua