diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 241256fb5ca2..45a1e0c6e8b8 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -39,7 +39,6 @@ from homeassistant.helpers.typing import ConfigType from .binary_sensor import BINARY_SENSOR_KEYS, BINARY_SENSORS, check_binary_sensors from .camera import STREAM_SOURCE_LIST from .const import ( - CAMERAS, COMM_RETRIES, COMM_TIMEOUT, DATA_AMCREST, @@ -359,7 +358,7 @@ def _start_event_monitor( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Amcrest IP Camera component.""" - hass.data.setdefault(DATA_AMCREST, {DEVICES: {}, CAMERAS: []}) + hass.data.setdefault(DATA_AMCREST, {DEVICES: {}}) for device in config[DOMAIN]: name: str = device[CONF_NAME] diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 5c3655e8d311..6f244c57f52b 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -12,13 +12,11 @@ import aiohttp from aiohttp import web from amcrest import AmcrestError from haffmpeg.camera import CameraMjpeg -import voluptuous as vol from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager -from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_stream, async_aiohttp_proxy_web, @@ -29,11 +27,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( + ATTR_COLOR_BW, CAMERA_WEB_SESSION_TIMEOUT, - CAMERAS, + CBW, COMM_TIMEOUT, DATA_AMCREST, DEVICES, + MOV, RESOLUTION_TO_STREAM, SERVICE_UPDATE, SNAPSHOT_TIMEOUT, @@ -49,65 +49,11 @@ SCAN_INTERVAL = timedelta(seconds=15) STREAM_SOURCE_LIST = ["snapshot", "mjpeg", "rtsp"] -_ATTR_PTZ_TT = "travel_time" -_ATTR_PTZ_MOV = "movement" -_MOV = [ - "zoom_out", - "zoom_in", - "right", - "left", - "up", - "down", - "right_down", - "right_up", - "left_down", - "left_up", -] _ZOOM_ACTIONS = ["ZoomWide", "ZoomTele"] _MOVE_1_ACTIONS = ["Right", "Left", "Up", "Down"] _MOVE_2_ACTIONS = ["RightDown", "RightUp", "LeftDown", "LeftUp"] _ACTION = _ZOOM_ACTIONS + _MOVE_1_ACTIONS + _MOVE_2_ACTIONS -_DEFAULT_TT = 0.2 - -_ATTR_PRESET = "preset" -_ATTR_COLOR_BW = "color_bw" - -_CBW_COLOR = "color" -_CBW_AUTO = "auto" -_CBW_BW = "bw" -_CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW] - -_SRV_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) -_SRV_GOTO_SCHEMA = _SRV_SCHEMA.extend( - {vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))} -) -_SRV_CBW_SCHEMA = _SRV_SCHEMA.extend({vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)}) -_SRV_PTZ_SCHEMA = _SRV_SCHEMA.extend( - { - vol.Required(_ATTR_PTZ_MOV): vol.In(_MOV), - vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float, - } -) - -CAMERA_SERVICES = { - "enable_recording": (_SRV_SCHEMA, "async_enable_recording", ()), - "disable_recording": (_SRV_SCHEMA, "async_disable_recording", ()), - "enable_audio": (_SRV_SCHEMA, "async_enable_audio", ()), - "disable_audio": (_SRV_SCHEMA, "async_disable_audio", ()), - "enable_motion_recording": (_SRV_SCHEMA, "async_enable_motion_recording", ()), - "disable_motion_recording": (_SRV_SCHEMA, "async_disable_motion_recording", ()), - "goto_preset": (_SRV_GOTO_SCHEMA, "async_goto_preset", (_ATTR_PRESET,)), - "set_color_bw": (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)), - "start_tour": (_SRV_SCHEMA, "async_start_tour", ()), - "stop_tour": (_SRV_SCHEMA, "async_stop_tour", ()), - "ptz_control": ( - _SRV_PTZ_SCHEMA, - "async_ptz_control", - (_ATTR_PTZ_MOV, _ATTR_PTZ_TT), - ), -} - _BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} @@ -275,7 +221,7 @@ class AmcrestCam(Camera): self._motion_recording_enabled ) if self._color_bw is not None: - attr[_ATTR_COLOR_BW] = self._color_bw + attr[ATTR_COLOR_BW] = self._color_bw return attr @property @@ -322,15 +268,7 @@ class AmcrestCam(Camera): self.async_schedule_update_ha_state(True) async def async_added_to_hass(self) -> None: - """Subscribe to signals and add camera to list.""" - self._unsub_dispatcher.extend( - async_dispatcher_connect( - self.hass, - service_signal(service, self.entity_id), - getattr(self, callback_name), - ) - for service, (_, callback_name, _) in CAMERA_SERVICES.items() - ) + """Subscribe to signals.""" self._unsub_dispatcher.append( async_dispatcher_connect( self.hass, @@ -338,11 +276,9 @@ class AmcrestCam(Camera): self.async_on_demand_update, ) ) - self.hass.data[DATA_AMCREST][CAMERAS].append(self.entity_id) async def async_will_remove_from_hass(self) -> None: - """Remove camera from list and disconnect from signals.""" - self.hass.data[DATA_AMCREST][CAMERAS].remove(self.entity_id) + """Disconnect from signals.""" for unsub_dispatcher in self._unsub_dispatcher: unsub_dispatcher() @@ -456,7 +392,7 @@ class AmcrestCam(Camera): async def async_ptz_control(self, movement: str, travel_time: float) -> None: """Move or zoom camera in specified direction.""" - code = _ACTION[_MOV.index(movement)] + code = _ACTION[MOV.index(movement)] kwargs = {"code": code, "arg1": 0, "arg2": 0, "arg3": 0} if code in _MOVE_1_ACTIONS: @@ -613,10 +549,10 @@ class AmcrestCam(Camera): ) async def _async_get_color_mode(self) -> str: - return _CBW[await self._api.async_day_night_color] + return CBW[await self._api.async_day_night_color] async def _async_set_color_mode(self, cbw: str) -> None: - await self._api.async_set_day_night_color(_CBW.index(cbw), channel=0) + await self._api.async_set_day_night_color(CBW.index(cbw), channel=0) async def _async_set_color_bw(self, cbw: str) -> None: """Set camera color mode.""" diff --git a/homeassistant/components/amcrest/const.py b/homeassistant/components/amcrest/const.py index 377c5642b4b7..67f37a826a28 100644 --- a/homeassistant/components/amcrest/const.py +++ b/homeassistant/components/amcrest/const.py @@ -2,7 +2,6 @@ DOMAIN = "amcrest" DATA_AMCREST = DOMAIN -CAMERAS = "cameras" DEVICES = "devices" BINARY_SENSOR_SCAN_INTERVAL_SECS = 5 @@ -17,3 +16,18 @@ SERVICE_UPDATE = "update" RESOLUTION_LIST = {"high": 0, "low": 1} RESOLUTION_TO_STREAM = {0: "Main", 1: "Extra"} + +ATTR_COLOR_BW = "color_bw" +CBW = ["color", "auto", "bw"] +MOV = [ + "zoom_out", + "zoom_in", + "right", + "left", + "up", + "down", + "right_down", + "right_up", + "left_down", + "left_up", +] diff --git a/homeassistant/components/amcrest/services.py b/homeassistant/components/amcrest/services.py index 6b4ca8ade535..8102ed005949 100644 --- a/homeassistant/components/amcrest/services.py +++ b/homeassistant/components/amcrest/services.py @@ -1,62 +1,67 @@ -"""Support for Amcrest IP cameras.""" +"""Services for Amcrest IP cameras.""" from __future__ import annotations -from homeassistant.auth.models import User -from homeassistant.auth.permissions.const import POLICY_CONTROL -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE -from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import Unauthorized, UnknownUser -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.service import async_extract_entity_ids +import voluptuous as vol -from .camera import CAMERA_SERVICES -from .const import CAMERAS, DATA_AMCREST, DOMAIN -from .helpers import service_signal +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, service + +from .const import ATTR_COLOR_BW, CBW, DOMAIN, MOV + +_ATTR_PRESET = "preset" +_ATTR_PTZ_MOV = "movement" +_ATTR_PTZ_TT = "travel_time" +_DEFAULT_TT = 0.2 @callback def async_setup_services(hass: HomeAssistant) -> None: """Set up the Amcrest IP Camera services.""" + for service_name, func in ( + ("enable_recording", "async_enable_recording"), + ("disable_recording", "async_disable_recording"), + ("enable_audio", "async_enable_audio"), + ("disable_audio", "async_disable_audio"), + ("enable_motion_recording", "async_enable_motion_recording"), + ("disable_motion_recording", "async_disable_motion_recording"), + ("start_tour", "async_start_tour"), + ("stop_tour", "async_stop_tour"), + ): + service.async_register_platform_entity_service( + hass, + DOMAIN, + service_name, + entity_domain=CAMERA_DOMAIN, + schema=None, + func=func, + ) - def have_permission(user: User | None, entity_id: str) -> bool: - return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL) - - async def async_extract_from_service(call: ServiceCall) -> list[str]: - if call.context.user_id: - user = await hass.auth.async_get_user(call.context.user_id) - if user is None: - raise UnknownUser(context=call.context) - else: - user = None - - if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL: - # Return all entity_ids user has permission to control. - return [ - entity_id - for entity_id in hass.data[DATA_AMCREST][CAMERAS] - if have_permission(user, entity_id) - ] - - if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE: - return [] - - call_ids = await async_extract_entity_ids(call) - entity_ids = [] - for entity_id in hass.data[DATA_AMCREST][CAMERAS]: - if entity_id not in call_ids: - continue - if not have_permission(user, entity_id): - raise Unauthorized( - context=call.context, entity_id=entity_id, permission=POLICY_CONTROL - ) - entity_ids.append(entity_id) - return entity_ids - - async def async_service_handler(call: ServiceCall) -> None: - args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]] - for entity_id in await async_extract_from_service(call): - async_dispatcher_send(hass, service_signal(call.service, entity_id), *args) - - for service, params in CAMERA_SERVICES.items(): - hass.services.async_register(DOMAIN, service, async_service_handler, params[0]) + service.async_register_platform_entity_service( + hass, + DOMAIN, + "goto_preset", + entity_domain=CAMERA_DOMAIN, + schema={vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))}, + func="async_goto_preset", + ) + service.async_register_platform_entity_service( + hass, + DOMAIN, + "set_color_bw", + entity_domain=CAMERA_DOMAIN, + schema={vol.Required(ATTR_COLOR_BW): vol.In(CBW)}, + func="async_set_color_bw", + ) + service.async_register_platform_entity_service( + hass, + DOMAIN, + "ptz_control", + entity_domain=CAMERA_DOMAIN, + schema={ + vol.Required(_ATTR_PTZ_MOV): vol.In(MOV), + vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float, + }, + func="async_ptz_control", + )