diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index 3342cc91744c..9e1cb97f7783 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -3,6 +3,7 @@ import logging from typing import Any, override +from pyhik.constants import SENSOR_MAP import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -43,115 +44,120 @@ DEFAULT_DELAY = 0 DEFAULT_IGNORED = False -# Entity descriptions for known Hikvision event types -# The key matches the sensor_type from pyhik (the friendly name from SENSOR_MAP) +# Keyed by the friendly names pyhik emits in `current_event_states`. Going +# through SENSOR_MAP keeps each description attached to the right event if +# pyhik renames a label. BINARY_SENSOR_DESCRIPTIONS: dict[str, BinarySensorEntityDescription] = { - "Motion": BinarySensorEntityDescription( + SENSOR_MAP["vmd"]: BinarySensorEntityDescription( key="motion", device_class=BinarySensorDeviceClass.MOTION, ), - "Line Crossing": BinarySensorEntityDescription( + SENSOR_MAP["linedetection"]: BinarySensorEntityDescription( key="line_crossing", translation_key="line_crossing", device_class=BinarySensorDeviceClass.MOTION, ), - "Field Detection": BinarySensorEntityDescription( + SENSOR_MAP["fielddetection"]: BinarySensorEntityDescription( key="field_detection", translation_key="field_detection", device_class=BinarySensorDeviceClass.MOTION, ), - "Tamper Detection": BinarySensorEntityDescription( + SENSOR_MAP["tamperdetection"]: BinarySensorEntityDescription( key="tamper_detection", device_class=BinarySensorDeviceClass.TAMPER, ), - "Disk Full": BinarySensorEntityDescription( + SENSOR_MAP["diskfull"]: BinarySensorEntityDescription( key="disk_full", translation_key="disk_full", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), - "Disk Error": BinarySensorEntityDescription( + SENSOR_MAP["diskerror"]: BinarySensorEntityDescription( key="disk_error", translation_key="disk_error", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), - "Net Interface Broken": BinarySensorEntityDescription( + SENSOR_MAP["nicbroken"]: BinarySensorEntityDescription( key="net_interface_broken", translation_key="net_interface_broken", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, ), - "IP Conflict": BinarySensorEntityDescription( + SENSOR_MAP["ipconflict"]: BinarySensorEntityDescription( key="ip_conflict", translation_key="ip_conflict", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), - "Illegal Access": BinarySensorEntityDescription( + SENSOR_MAP["illaccess"]: BinarySensorEntityDescription( key="illegal_access", translation_key="illegal_access", device_class=BinarySensorDeviceClass.SAFETY, ), - "Video Mismatch": BinarySensorEntityDescription( + SENSOR_MAP["videomismatch"]: BinarySensorEntityDescription( key="video_mismatch", translation_key="video_mismatch", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), - "Bad Video": BinarySensorEntityDescription( + SENSOR_MAP["badvideo"]: BinarySensorEntityDescription( key="bad_video", translation_key="bad_video", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), - "PIR Alarm": BinarySensorEntityDescription( + SENSOR_MAP["pir"]: BinarySensorEntityDescription( key="pir_alarm", translation_key="pir_alarm", device_class=BinarySensorDeviceClass.MOTION, ), - "Face Detection": BinarySensorEntityDescription( + SENSOR_MAP["facedetection"]: BinarySensorEntityDescription( key="face_detection", translation_key="face_detection", device_class=BinarySensorDeviceClass.MOTION, ), - "Scene Change Detection": BinarySensorEntityDescription( + SENSOR_MAP["scenechangedetection"]: BinarySensorEntityDescription( key="scene_change_detection", translation_key="scene_change_detection", device_class=BinarySensorDeviceClass.MOTION, ), - "I/O": BinarySensorEntityDescription( + SENSOR_MAP["io"]: BinarySensorEntityDescription( key="io", translation_key="io", ), - "Unattended Baggage": BinarySensorEntityDescription( + SENSOR_MAP["unattendedbaggage"]: BinarySensorEntityDescription( key="unattended_baggage", translation_key="unattended_baggage", device_class=BinarySensorDeviceClass.MOTION, ), - "Attended Baggage": BinarySensorEntityDescription( + SENSOR_MAP["attendedbaggage"]: BinarySensorEntityDescription( key="attended_baggage", translation_key="attended_baggage", device_class=BinarySensorDeviceClass.MOTION, ), - "Recording Failure": BinarySensorEntityDescription( + SENSOR_MAP["recordingfailure"]: BinarySensorEntityDescription( key="recording_failure", translation_key="recording_failure", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), - "Exiting Region": BinarySensorEntityDescription( + SENSOR_MAP["regionexiting"]: BinarySensorEntityDescription( key="exiting_region", translation_key="exiting_region", device_class=BinarySensorDeviceClass.MOTION, ), - "Entering Region": BinarySensorEntityDescription( + SENSOR_MAP["regionentrance"]: BinarySensorEntityDescription( key="entering_region", translation_key="entering_region", device_class=BinarySensorDeviceClass.MOTION, ), } +# pyhik uses videoloss as a connection watchdog, not a user-facing event, so a +# leaked entry gets no entity and no unknown-sensor warning. +IGNORED_SENSOR_TYPES: frozenset[str] = frozenset({SENSOR_MAP["videoloss"]}) + _LOGGER = logging.getLogger(__name__) CUSTOMIZE_SCHEMA = vol.Schema( @@ -243,9 +249,11 @@ 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: + if ( + sensor_type not in BINARY_SENSOR_DESCRIPTIONS + and sensor_type not in IGNORED_SENSOR_TYPES + ): _LOGGER.warning( "Unknown Hikvision sensor type '%s', please report this at " "https://github.com/home-assistant/core/issues", diff --git a/tests/components/hikvision/test_binary_sensor.py b/tests/components/hikvision/test_binary_sensor.py index 7ed77ca9462a..5cc742971e1c 100644 --- a/tests/components/hikvision/test_binary_sensor.py +++ b/tests/components/hikvision/test_binary_sensor.py @@ -3,6 +3,7 @@ import logging from unittest.mock import MagicMock +from pyhik.constants import SENSOR_MAP import pytest from syrupy.assertion import SnapshotAssertion @@ -249,6 +250,25 @@ async def test_binary_sensor_device_class_unknown( assert "Unknown Hikvision sensor type 'Unknown Event'" in caplog.text +async def test_binary_sensor_videoloss_silently_skipped( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_hikcamera: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test pyhik's videoloss watchdog event is skipped without warning.""" + mock_hikcamera.return_value.current_event_states = { + SENSOR_MAP["videoloss"]: [(False, 1)], + } + + with caplog.at_level(logging.WARNING): + await setup_integration(hass, mock_config_entry) + + states = hass.states.async_entity_ids("binary_sensor") + assert len(states) == 0 + assert "Unknown Hikvision sensor type" not in caplog.text + + async def test_yaml_import_creates_deprecation_issue( hass: HomeAssistant, mock_hikcamera: MagicMock,