mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Add motionEye sensor platform (#53415)
This commit is contained in:
parent
a2102deb64
commit
944a7c09c4
@ -31,6 +31,7 @@ from motioneye_client.const import (
|
||||
)
|
||||
|
||||
from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.components.webhook import (
|
||||
async_generate_id,
|
||||
@ -82,7 +83,7 @@ from .const import (
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = [CAMERA_DOMAIN, SWITCH_DOMAIN]
|
||||
PLATFORMS = [CAMERA_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN]
|
||||
|
||||
|
||||
def create_motioneye_client(
|
||||
@ -478,3 +479,8 @@ class MotionEyeEntity(CoordinatorEntity):
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device information."""
|
||||
return DeviceInfo(identifiers={self._device_identifier})
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return self._camera is not None and super().available
|
||||
|
@ -84,6 +84,7 @@ MOTIONEYE_MANUFACTURER: Final = "motionEye"
|
||||
SIGNAL_CAMERA_ADD: Final = f"{DOMAIN}_camera_add_signal." "{}"
|
||||
SIGNAL_CAMERA_REMOVE: Final = f"{DOMAIN}_camera_remove_signal." "{}"
|
||||
|
||||
TYPE_MOTIONEYE_ACTION_SENSOR = f"{DOMAIN}_action_sensor"
|
||||
TYPE_MOTIONEYE_MJPEG_CAMERA: Final = "motioneye_mjpeg_camera"
|
||||
TYPE_MOTIONEYE_SWITCH_BASE: Final = f"{DOMAIN}_switch"
|
||||
|
||||
|
94
homeassistant/components/motioneye/sensor.py
Normal file
94
homeassistant/components/motioneye/sensor.py
Normal file
@ -0,0 +1,94 @@
|
||||
"""Sensor platform for motionEye."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from motioneye_client.client import MotionEyeClient
|
||||
from motioneye_client.const import KEY_ACTIONS, KEY_NAME
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import MotionEyeEntity, get_camera_from_cameras, listen_for_new_cameras
|
||||
from .const import CONF_CLIENT, CONF_COORDINATOR, DOMAIN, TYPE_MOTIONEYE_ACTION_SENSOR
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up motionEye from a config entry."""
|
||||
entry_data = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
@callback
|
||||
def camera_add(camera: dict[str, Any]) -> None:
|
||||
"""Add a new motionEye camera."""
|
||||
async_add_entities(
|
||||
[
|
||||
MotionEyeActionSensor(
|
||||
entry.entry_id,
|
||||
camera,
|
||||
entry_data[CONF_CLIENT],
|
||||
entry_data[CONF_COORDINATOR],
|
||||
entry.options,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
listen_for_new_cameras(hass, entry, camera_add)
|
||||
|
||||
|
||||
class MotionEyeActionSensor(MotionEyeEntity, SensorEntity):
|
||||
"""motionEye action sensor camera."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry_id: str,
|
||||
camera: dict[str, Any],
|
||||
client: MotionEyeClient,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
options: MappingProxyType[str, str],
|
||||
) -> None:
|
||||
"""Initialize an action sensor."""
|
||||
super().__init__(
|
||||
config_entry_id,
|
||||
TYPE_MOTIONEYE_ACTION_SENSOR,
|
||||
camera,
|
||||
client,
|
||||
coordinator,
|
||||
options,
|
||||
SensorEntityDescription(
|
||||
key=TYPE_MOTIONEYE_ACTION_SENSOR, entity_registry_enabled_default=False
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
camera_prepend = f"{self._camera[KEY_NAME]} " if self._camera else ""
|
||||
return f"{camera_prepend}Actions"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the value reported by the sensor."""
|
||||
return len(self._camera.get(KEY_ACTIONS, [])) if self._camera else 0
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Add actions as attribute."""
|
||||
if actions := (self._camera.get(KEY_ACTIONS) if self._camera else None):
|
||||
return {KEY_ACTIONS: actions}
|
||||
return None
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._camera = get_camera_from_cameras(self._camera_id, self.coordinator.data)
|
||||
super()._handle_coordinator_update()
|
@ -6,11 +6,13 @@ from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from motioneye_client.const import DEFAULT_PORT
|
||||
|
||||
from homeassistant.components.motioneye import get_motioneye_entity_unique_id
|
||||
from homeassistant.components.motioneye.const import DOMAIN
|
||||
from homeassistant.config import async_process_ha_core_config
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@ -23,7 +25,7 @@ TEST_CAMERA_DEVICE_IDENTIFIER = (DOMAIN, f"{TEST_CONFIG_ENTRY_ID}_{TEST_CAMERA_I
|
||||
TEST_CAMERA = {
|
||||
"show_frame_changes": False,
|
||||
"framerate": 25,
|
||||
"actions": [],
|
||||
"actions": ["one", "two", "three"],
|
||||
"preserve_movies": 0,
|
||||
"auto_threshold_tuning": True,
|
||||
"recording_mode": "motion-triggered",
|
||||
@ -133,6 +135,7 @@ TEST_CAMERA = {
|
||||
}
|
||||
TEST_CAMERAS = {"cameras": [TEST_CAMERA]}
|
||||
TEST_SURVEILLANCE_USERNAME = "surveillance_username"
|
||||
TEST_SENSOR_ACTION_ENTITY_ID = "sensor.test_camera_actions"
|
||||
TEST_SWITCH_ENTITY_ID_BASE = "switch.test_camera"
|
||||
TEST_SWITCH_MOTION_DETECTION_ENTITY_ID = (
|
||||
f"{TEST_SWITCH_ENTITY_ID_BASE}_motion_detection"
|
||||
@ -189,3 +192,23 @@ async def setup_mock_motioneye_config_entry(
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return config_entry
|
||||
|
||||
|
||||
def register_test_entity(
|
||||
hass: HomeAssistant, platform: str, camera_id: int, type_name: str, entity_id: str
|
||||
) -> None:
|
||||
"""Register a test entity."""
|
||||
|
||||
unique_id = get_motioneye_entity_unique_id(
|
||||
TEST_CONFIG_ENTRY_ID, camera_id, type_name
|
||||
)
|
||||
entity_id = entity_id.split(".")[1]
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
entity_registry.async_get_or_create(
|
||||
platform,
|
||||
DOMAIN,
|
||||
unique_id,
|
||||
suggested_object_id=entity_id,
|
||||
disabled_by=None,
|
||||
)
|
||||
|
@ -222,12 +222,12 @@ async def test_get_still_image_from_camera(
|
||||
server = await aiohttp_server(app)
|
||||
client = create_mock_motioneye_client()
|
||||
client.get_camera_snapshot_url = Mock(
|
||||
return_value=f"http://localhost:{server.port}/foo"
|
||||
return_value=f"http://127.0.0.1:{server.port}/foo"
|
||||
)
|
||||
config_entry = create_mock_motioneye_config_entry(
|
||||
hass,
|
||||
data={
|
||||
CONF_URL: f"http://localhost:{server.port}",
|
||||
CONF_URL: f"http://127.0.0.1:{server.port}",
|
||||
CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME,
|
||||
},
|
||||
)
|
||||
|
132
tests/components/motioneye/test_sensor.py
Normal file
132
tests/components/motioneye/test_sensor.py
Normal file
@ -0,0 +1,132 @@
|
||||
"""Tests for the motionEye switch platform."""
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from motioneye_client.const import KEY_ACTIONS
|
||||
|
||||
from homeassistant.components.motioneye import get_motioneye_device_identifier
|
||||
from homeassistant.components.motioneye.const import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
TYPE_MOTIONEYE_ACTION_SENSOR,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import (
|
||||
TEST_CAMERA,
|
||||
TEST_CAMERA_ID,
|
||||
TEST_SENSOR_ACTION_ENTITY_ID,
|
||||
create_mock_motioneye_client,
|
||||
register_test_entity,
|
||||
setup_mock_motioneye_config_entry,
|
||||
)
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
async def test_sensor_actions(hass: HomeAssistant) -> None:
|
||||
"""Test the actions sensor."""
|
||||
register_test_entity(
|
||||
hass,
|
||||
SENSOR_DOMAIN,
|
||||
TEST_CAMERA_ID,
|
||||
TYPE_MOTIONEYE_ACTION_SENSOR,
|
||||
TEST_SENSOR_ACTION_ENTITY_ID,
|
||||
)
|
||||
|
||||
client = create_mock_motioneye_client()
|
||||
await setup_mock_motioneye_config_entry(hass, client=client)
|
||||
|
||||
entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID)
|
||||
assert entity_state
|
||||
assert entity_state.state == "3"
|
||||
assert entity_state.attributes.get(KEY_ACTIONS) == ["one", "two", "three"]
|
||||
|
||||
updated_camera = copy.deepcopy(TEST_CAMERA)
|
||||
updated_camera[KEY_ACTIONS] = ["one"]
|
||||
|
||||
# When the next refresh is called return the updated values.
|
||||
client.async_get_cameras = AsyncMock(return_value={"cameras": [updated_camera]})
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID)
|
||||
assert entity_state
|
||||
assert entity_state.state == "1"
|
||||
assert entity_state.attributes.get(KEY_ACTIONS) == ["one"]
|
||||
|
||||
del updated_camera[KEY_ACTIONS]
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID)
|
||||
assert entity_state
|
||||
assert entity_state.state == "0"
|
||||
assert entity_state.attributes.get(KEY_ACTIONS) is None
|
||||
|
||||
|
||||
async def test_sensor_device_info(hass: HomeAssistant) -> None:
|
||||
"""Verify device information includes expected details."""
|
||||
|
||||
# Enable the action sensor (it is disabled by default).
|
||||
register_test_entity(
|
||||
hass,
|
||||
SENSOR_DOMAIN,
|
||||
TEST_CAMERA_ID,
|
||||
TYPE_MOTIONEYE_ACTION_SENSOR,
|
||||
TEST_SENSOR_ACTION_ENTITY_ID,
|
||||
)
|
||||
|
||||
config_entry = await setup_mock_motioneye_config_entry(hass)
|
||||
|
||||
device_identifer = get_motioneye_device_identifier(
|
||||
config_entry.entry_id, TEST_CAMERA_ID
|
||||
)
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get_device({device_identifer})
|
||||
assert device
|
||||
|
||||
entity_registry = await er.async_get_registry(hass)
|
||||
entities_from_device = [
|
||||
entry.entity_id
|
||||
for entry in er.async_entries_for_device(entity_registry, device.id)
|
||||
]
|
||||
assert TEST_SENSOR_ACTION_ENTITY_ID in entities_from_device
|
||||
|
||||
|
||||
async def test_sensor_actions_can_be_enabled(hass: HomeAssistant) -> None:
|
||||
"""Verify the action sensor can be enabled."""
|
||||
client = create_mock_motioneye_client()
|
||||
await setup_mock_motioneye_config_entry(hass, client=client)
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
entry = entity_registry.async_get(TEST_SENSOR_ACTION_ENTITY_ID)
|
||||
assert entry
|
||||
assert entry.disabled
|
||||
assert entry.disabled_by == er.DISABLED_INTEGRATION
|
||||
entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID)
|
||||
assert not entity_state
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.motioneye.MotionEyeClient",
|
||||
return_value=client,
|
||||
):
|
||||
updated_entry = entity_registry.async_update_entity(
|
||||
TEST_SENSOR_ACTION_ENTITY_ID, disabled_by=None
|
||||
)
|
||||
assert not updated_entry.disabled
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID)
|
||||
assert entity_state
|
Loading…
x
Reference in New Issue
Block a user