mirror of
https://github.com/home-assistant/core.git
synced 2026-09-27 01:46:11 -04:00
Hoist Hikvision entity availability to the base entity (#182306)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
407307ce30
commit
e8e5a55055
@@ -314,12 +314,6 @@ class HikvisionBinarySensor(HikvisionEntity, BinarySensorEntity):
|
||||
"""Get sensor attributes from camera."""
|
||||
return self._camera.fetch_attributes(self._sensor_type, self._channel)
|
||||
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return true if the device's event stream is connected."""
|
||||
return self._camera.stream_connected
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool:
|
||||
@@ -332,19 +326,3 @@ class HikvisionBinarySensor(HikvisionEntity, BinarySensorEntity):
|
||||
"""Return the state attributes."""
|
||||
attrs = self._get_sensor_attributes()
|
||||
return {ATTR_LAST_TRIP_TIME: attrs[3]}
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callback when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Register callback with pyhik
|
||||
self._camera.add_update_callback(self._update_callback, self._callback_id)
|
||||
|
||||
def _update_callback(self, msg: str) -> None:
|
||||
"""Update the sensor's state when callback is triggered.
|
||||
|
||||
This is called from pyhik's event stream thread, so we use
|
||||
schedule_update_ha_state which is thread-safe.
|
||||
"""
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -63,6 +63,10 @@ class HikvisionCamera(HikvisionEntity, Camera):
|
||||
# Build unique ID (unique per platform per integration)
|
||||
self._attr_unique_id = f"{self._data.device_id}_{channel.id}"
|
||||
|
||||
# No pyhik event is routed here; the registration exists so the
|
||||
# camera is told when the event stream connects or drops.
|
||||
self._callback_id = f"{self._data.device_id}.camera.{channel.id}"
|
||||
|
||||
@override
|
||||
async def async_camera_image(
|
||||
self, width: int | None = None, height: int | None = None
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Base entity for Hikvision integration."""
|
||||
|
||||
from typing import override
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -14,6 +16,12 @@ class HikvisionEntity(Entity):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
# pyhik routes an update to the callbacks registered under this exact
|
||||
# identifier and passes it back as the callback message. It also
|
||||
# broadcasts to every registered callback when the event stream
|
||||
# connects or drops, which is what drives availability.
|
||||
_callback_id: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
@@ -49,3 +57,29 @@ class HikvisionEntity(Entity):
|
||||
manufacturer="Hikvision",
|
||||
model=self._data.device_type,
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return true if the device's event stream is connected."""
|
||||
return self._camera.stream_connected
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callback when entity is added, and drop it on removal."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
self._camera.add_update_callback(self._update_callback, self._callback_id)
|
||||
self.async_on_remove(
|
||||
lambda: self._camera.remove_update_callback(
|
||||
self._update_callback, self._callback_id
|
||||
)
|
||||
)
|
||||
|
||||
def _update_callback(self, msg: str) -> None:
|
||||
"""Update the entity state when the callback is triggered.
|
||||
|
||||
This is called from pyhik event stream thread, so we use
|
||||
schedule_update_ha_state which is thread-safe.
|
||||
"""
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -135,19 +135,7 @@ class HikvisionEvent(HikvisionEntity, EventEntity):
|
||||
"""Get sensor attributes from camera."""
|
||||
return self._camera.fetch_attributes(self._sensor_type, self._channel)
|
||||
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return true if the device's event stream is connected."""
|
||||
return self._camera.stream_connected
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callback when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
self._camera.add_update_callback(self._update_callback, self._callback_id)
|
||||
|
||||
def _update_callback(self, msg: str) -> None:
|
||||
"""Handle an update from pyhik's event stream thread."""
|
||||
# Read the state on the callback thread: a trip that has already ended
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhik"],
|
||||
"requirements": ["pyHik==0.4.6"]
|
||||
"requirements": ["pyHik==0.4.7"]
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -2038,7 +2038,7 @@ pyElectra==1.2.4
|
||||
pyEmby==1.10
|
||||
|
||||
# homeassistant.components.hikvision
|
||||
pyHik==0.4.6
|
||||
pyHik==0.4.7
|
||||
|
||||
# homeassistant.components.homee
|
||||
pyHomee==1.4.4
|
||||
|
||||
@@ -7,7 +7,7 @@ from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.camera import async_get_image, async_get_stream_source
|
||||
from homeassistant.components.hikvision.const import DOMAIN
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
@@ -167,3 +167,51 @@ async def test_camera_stream_source(
|
||||
|
||||
# Verify get_stream_url was called with channel 1
|
||||
mock_hikcamera.return_value.get_stream_url.assert_called_with(1)
|
||||
|
||||
|
||||
async def test_camera_unavailable_when_stream_disconnected(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_hikcamera: MagicMock,
|
||||
) -> None:
|
||||
"""Test the camera goes unavailable when the event stream disconnects."""
|
||||
camera = mock_hikcamera.return_value
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("camera.front_camera")
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
# pyhik notifies every registered callback when the stream drops
|
||||
camera.stream_connected = False
|
||||
callback_func = camera.add_update_callback.call_args_list[0][0][0]
|
||||
callback_func("stream disconnected")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("camera.front_camera")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_camera_callback_removed_with_entity(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_hikcamera: MagicMock,
|
||||
) -> None:
|
||||
"""Test the pyhik callback is unregistered when the entity is removed."""
|
||||
camera = mock_hikcamera.return_value
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
added = [
|
||||
c.args
|
||||
for c in camera.add_update_callback.call_args_list
|
||||
if ".camera." in c.args[1]
|
||||
]
|
||||
assert len(added) == 1
|
||||
camera.remove_update_callback.assert_not_called()
|
||||
|
||||
entity_registry.async_remove("camera.front_camera")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
camera.remove_update_callback.assert_called_once_with(*added[0])
|
||||
|
||||
Reference in New Issue
Block a user