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:
@@ -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}"
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user