1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Add entity descriptions to Hikvision binary sensors (#160875)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Paul Tarjan
2026-02-14 13:32:39 -07:00
committed by GitHub
parent 82fb3c35dc
commit 8840d2f0ef
4 changed files with 225 additions and 42 deletions

View File

@@ -11,6 +11,7 @@ from homeassistant.components.binary_sensor import (
PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA,
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
@@ -23,6 +24,7 @@ from homeassistant.const import (
CONF_PORT,
CONF_SSL,
CONF_USERNAME,
EntityCategory,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
@@ -42,29 +44,118 @@ CONF_IGNORED = "ignored"
DEFAULT_DELAY = 0
DEFAULT_IGNORED = False
# Device class mapping for Hikvision event types
DEVICE_CLASS_MAP: dict[str, BinarySensorDeviceClass | None] = {
"Motion": BinarySensorDeviceClass.MOTION,
"Line Crossing": BinarySensorDeviceClass.MOTION,
"Field Detection": BinarySensorDeviceClass.MOTION,
"Tamper Detection": BinarySensorDeviceClass.MOTION,
"Shelter Alarm": None,
"Disk Full": None,
"Disk Error": None,
"Net Interface Broken": BinarySensorDeviceClass.CONNECTIVITY,
"IP Conflict": BinarySensorDeviceClass.CONNECTIVITY,
"Illegal Access": None,
"Video Mismatch": None,
"Bad Video": None,
"PIR Alarm": BinarySensorDeviceClass.MOTION,
"Face Detection": BinarySensorDeviceClass.MOTION,
"Scene Change Detection": BinarySensorDeviceClass.MOTION,
"I/O": None,
"Unattended Baggage": BinarySensorDeviceClass.MOTION,
"Attended Baggage": BinarySensorDeviceClass.MOTION,
"Recording Failure": None,
"Exiting Region": BinarySensorDeviceClass.MOTION,
"Entering Region": BinarySensorDeviceClass.MOTION,
# Entity descriptions for known Hikvision event types
# The key matches the sensor_type from pyhik (the friendly name from SENSOR_MAP)
BINARY_SENSOR_DESCRIPTIONS: dict[str, BinarySensorEntityDescription] = {
"Motion": BinarySensorEntityDescription(
key="motion",
device_class=BinarySensorDeviceClass.MOTION,
),
"Line Crossing": BinarySensorEntityDescription(
key="line_crossing",
translation_key="line_crossing",
device_class=BinarySensorDeviceClass.MOTION,
),
"Field Detection": BinarySensorEntityDescription(
key="field_detection",
translation_key="field_detection",
device_class=BinarySensorDeviceClass.MOTION,
),
"Tamper Detection": BinarySensorEntityDescription(
key="tamper_detection",
device_class=BinarySensorDeviceClass.TAMPER,
),
"Shelter Alarm": BinarySensorEntityDescription(
key="shelter_alarm",
translation_key="shelter_alarm",
),
"Disk Full": BinarySensorEntityDescription(
key="disk_full",
translation_key="disk_full",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
"Disk Error": BinarySensorEntityDescription(
key="disk_error",
translation_key="disk_error",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
"Net Interface Broken": BinarySensorEntityDescription(
key="net_interface_broken",
translation_key="net_interface_broken",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
),
"IP Conflict": BinarySensorEntityDescription(
key="ip_conflict",
translation_key="ip_conflict",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
"Illegal Access": BinarySensorEntityDescription(
key="illegal_access",
translation_key="illegal_access",
device_class=BinarySensorDeviceClass.SAFETY,
),
"Video Mismatch": BinarySensorEntityDescription(
key="video_mismatch",
translation_key="video_mismatch",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
"Bad Video": BinarySensorEntityDescription(
key="bad_video",
translation_key="bad_video",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
"PIR Alarm": BinarySensorEntityDescription(
key="pir_alarm",
translation_key="pir_alarm",
device_class=BinarySensorDeviceClass.MOTION,
),
"Face Detection": BinarySensorEntityDescription(
key="face_detection",
translation_key="face_detection",
device_class=BinarySensorDeviceClass.MOTION,
),
"Scene Change Detection": BinarySensorEntityDescription(
key="scene_change_detection",
translation_key="scene_change_detection",
device_class=BinarySensorDeviceClass.MOTION,
),
"I/O": BinarySensorEntityDescription(
key="io",
translation_key="io",
),
"Unattended Baggage": BinarySensorEntityDescription(
key="unattended_baggage",
translation_key="unattended_baggage",
device_class=BinarySensorDeviceClass.MOTION,
),
"Attended Baggage": BinarySensorEntityDescription(
key="attended_baggage",
translation_key="attended_baggage",
device_class=BinarySensorDeviceClass.MOTION,
),
"Recording Failure": BinarySensorEntityDescription(
key="recording_failure",
translation_key="recording_failure",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
"Exiting Region": BinarySensorEntityDescription(
key="exiting_region",
translation_key="exiting_region",
device_class=BinarySensorDeviceClass.MOTION,
),
"Entering Region": BinarySensorEntityDescription(
key="entering_region",
translation_key="entering_region",
device_class=BinarySensorDeviceClass.MOTION,
),
}
_LOGGER = logging.getLogger(__name__)
@@ -158,13 +249,24 @@ async def async_setup_entry(
)
return
# Log warnings for unknown sensor types and skip them
for sensor_type in sensors:
if sensor_type not in BINARY_SENSOR_DESCRIPTIONS:
_LOGGER.warning(
"Unknown Hikvision sensor type '%s', please report this at "
"https://github.com/home-assistant/core/issues",
sensor_type,
)
async_add_entities(
HikvisionBinarySensor(
entry=entry,
description=BINARY_SENSOR_DESCRIPTIONS[sensor_type],
sensor_type=sensor_type,
channel=channel_info[1],
)
for sensor_type, channel_list in sensors.items()
if sensor_type in BINARY_SENSOR_DESCRIPTIONS
for channel_info in channel_list
)
@@ -177,20 +279,18 @@ class HikvisionBinarySensor(HikvisionEntity, BinarySensorEntity):
def __init__(
self,
entry: HikvisionConfigEntry,
description: BinarySensorEntityDescription,
sensor_type: str,
channel: int,
) -> None:
"""Initialize the binary sensor."""
super().__init__(entry, channel)
self.entity_description = description
self._sensor_type = sensor_type
# Build unique ID (includes sensor_type for uniqueness per sensor)
self._attr_unique_id = f"{self._data.device_id}_{sensor_type}_{channel}"
# Set entity name and device class
self._attr_name = sensor_type
self._attr_device_class = DEVICE_CLASS_MAP.get(sensor_type)
# Callback ID for pyhik
self._callback_id = f"{self._data.device_id}.{sensor_type}.{channel}"

View File

@@ -34,6 +34,67 @@
"name": "{device_name} channel {channel_number}"
}
},
"entity": {
"binary_sensor": {
"attended_baggage": {
"name": "Attended baggage"
},
"bad_video": {
"name": "Bad video"
},
"disk_error": {
"name": "Disk error"
},
"disk_full": {
"name": "Disk full"
},
"entering_region": {
"name": "Entering region"
},
"exiting_region": {
"name": "Exiting region"
},
"face_detection": {
"name": "Face detection"
},
"field_detection": {
"name": "Field detection"
},
"illegal_access": {
"name": "Illegal access"
},
"io": {
"name": "I/O alarm"
},
"ip_conflict": {
"name": "IP conflict"
},
"line_crossing": {
"name": "Line crossing"
},
"net_interface_broken": {
"name": "Network interface broken"
},
"pir_alarm": {
"name": "PIR alarm"
},
"recording_failure": {
"name": "Recording failure"
},
"scene_change_detection": {
"name": "Scene change detection"
},
"shelter_alarm": {
"name": "Shelter alarm"
},
"unattended_baggage": {
"name": "Unattended baggage"
},
"video_mismatch": {
"name": "Video mismatch"
}
}
},
"issues": {
"deprecated_yaml_import_issue": {
"description": "Configuring {integration_title} using YAML is deprecated and the import failed. Please remove the `{domain}` entry from your `configuration.yaml` file and set up the integration manually.",

View File

@@ -20,17 +20,17 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Line Crossing',
'object_id_base': 'Line crossing',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.MOTION: 'motion'>,
'original_icon': None,
'original_name': 'Line Crossing',
'original_name': 'Line crossing',
'platform': 'hikvision',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'translation_key': 'line_crossing',
'unique_id': 'DS-2CD2142FWD-I20170101AAAA_Line Crossing_1',
'unit_of_measurement': None,
})
@@ -39,7 +39,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'motion',
'friendly_name': 'Front Camera Line Crossing',
'friendly_name': 'Front Camera Line crossing',
'last_tripped_time': '2024-01-01T00:00:00Z',
}),
'context': <ANY>,

View File

@@ -1,5 +1,6 @@
"""Test Hikvision binary sensors."""
import logging
from unittest.mock import MagicMock
import pytest
@@ -115,22 +116,28 @@ async def test_binary_sensor_no_sensors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_hikcamera: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test setup when device has no sensors."""
mock_hikcamera.return_value.current_event_states = None
await setup_integration(hass, mock_config_entry)
with caplog.at_level(logging.WARNING):
await setup_integration(hass, mock_config_entry)
# No binary sensors should be created
states = hass.states.async_entity_ids("binary_sensor")
assert len(states) == 0
# Verify warning was logged
assert "has no sensors available" in caplog.text
@pytest.mark.parametrize("amount_of_channels", [2])
async def test_binary_sensor_nvr_device(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_hikcamera: MagicMock,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test binary sensor naming for NVR devices."""
mock_hikcamera.return_value.get_type = "NVR"
@@ -140,12 +147,22 @@ async def test_binary_sensor_nvr_device(
await setup_integration(hass, mock_config_entry)
# NVR sensors are on per-channel devices
state = hass.states.get("binary_sensor.front_camera_channel_1_motion")
assert state is not None
# Verify NVR channel devices are created with via_device linking
channel_1_device = device_registry.async_get_device(
identifiers={(DOMAIN, f"{TEST_DEVICE_ID}_1")}
)
assert channel_1_device is not None
assert channel_1_device.via_device_id is not None
state = hass.states.get("binary_sensor.front_camera_channel_2_motion")
assert state is not None
channel_2_device = device_registry.async_get_device(
identifiers={(DOMAIN, f"{TEST_DEVICE_ID}_2")}
)
assert channel_2_device is not None
assert channel_2_device.via_device_id is not None
# Verify sensors are created (entity IDs depend on translation loading)
states = hass.states.async_entity_ids("binary_sensor")
assert len(states) == 2
async def test_binary_sensor_state_on(
@@ -172,17 +189,22 @@ async def test_binary_sensor_device_class_unknown(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_hikcamera: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test binary sensor with unknown device class."""
"""Test unknown sensor types are logged and skipped."""
mock_hikcamera.return_value.current_event_states = {
"Unknown Event": [(False, 1)],
}
await setup_integration(hass, mock_config_entry)
with caplog.at_level(logging.WARNING):
await setup_integration(hass, mock_config_entry)
state = hass.states.get("binary_sensor.front_camera_unknown_event")
assert state is not None
assert state.attributes.get(ATTR_DEVICE_CLASS) is None
# No entity should be created for unknown sensor types
states = hass.states.async_entity_ids("binary_sensor")
assert len(states) == 0
# Verify warning was logged for unknown sensor type
assert "Unknown Hikvision sensor type 'Unknown Event'" in caplog.text
async def test_yaml_import_creates_deprecation_issue(