Add motionEye sensor platform (#53415)

This commit is contained in:
Dermot Duffy 2021-10-29 14:14:26 -07:00 committed by GitHub
parent a2102deb64
commit 944a7c09c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 260 additions and 4 deletions

View File

@ -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

View File

@ -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"

View 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()

View File

@ -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,
)

View File

@ -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,
},
)

View 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