"""Support for Netgear Arlo IP cameras.""" from __future__ import annotations import logging from haffmpeg.camera import CameraMjpeg import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DATA_ARLO, DEFAULT_BRAND, SIGNAL_UPDATE_ARLO _LOGGER = logging.getLogger(__name__) ARLO_MODE_ARMED = "armed" ARLO_MODE_DISARMED = "disarmed" ATTR_BRIGHTNESS = "brightness" ATTR_FLIPPED = "flipped" ATTR_MIRRORED = "mirrored" ATTR_MOTION = "motion_detection_sensitivity" ATTR_POWERSAVE = "power_save_mode" ATTR_SIGNAL_STRENGTH = "signal_strength" ATTR_UNSEEN_VIDEOS = "unseen_videos" ATTR_LAST_REFRESH = "last_refresh" CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" DEFAULT_ARGUMENTS = "-pred 1" POWERSAVE_MODE_MAPPING = {1: "best_battery_life", 2: "optimized", 3: "best_video"} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string} ) def setup_platform( hass: HomeAssistant, config: ConfigType, add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up an Arlo IP Camera.""" arlo = hass.data[DATA_ARLO] cameras = [] for camera in arlo.cameras: cameras.append(ArloCam(hass, camera, config)) add_entities(cameras) class ArloCam(Camera): """An implementation of a Netgear Arlo IP camera.""" def __init__(self, hass, camera, device_info): """Initialize an Arlo camera.""" super().__init__() self._camera = camera self._attr_name = camera.name self._motion_status = False self._ffmpeg = get_ffmpeg_manager(hass) self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._last_refresh = None self.attrs = {} def camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" return self._camera.last_image_from_cache async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_UPDATE_ARLO, self.async_write_ha_state ) ) async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" video = await self.hass.async_add_executor_job( getattr, self._camera, "last_video" ) if not video: error_msg = ( f"Video not found for {self.name}. " f"Is it older than {self._camera.min_days_vdo_cache} days?" ) _LOGGER.error(error_msg) return stream = CameraMjpeg(self._ffmpeg.binary) await stream.open_camera(video.video_url, extra_cmd=self._ffmpeg_arguments) try: stream_reader = await stream.get_reader() return await async_aiohttp_proxy_stream( self.hass, request, stream_reader, self._ffmpeg.ffmpeg_stream_content_type, ) finally: await stream.close() @property def extra_state_attributes(self): """Return the state attributes.""" return { name: value for name, value in ( (ATTR_BATTERY_LEVEL, self._camera.battery_level), (ATTR_BRIGHTNESS, self._camera.brightness), (ATTR_FLIPPED, self._camera.flip_state), (ATTR_MIRRORED, self._camera.mirror_state), (ATTR_MOTION, self._camera.motion_detection_sensitivity), ( ATTR_POWERSAVE, POWERSAVE_MODE_MAPPING.get(self._camera.powersave_mode), ), (ATTR_SIGNAL_STRENGTH, self._camera.signal_strength), (ATTR_UNSEEN_VIDEOS, self._camera.unseen_videos), ) if value is not None } @property def model(self): """Return the camera model.""" return self._camera.model_id @property def brand(self): """Return the camera brand.""" return DEFAULT_BRAND @property def motion_detection_enabled(self): """Return the camera motion detection status.""" return self._motion_status def set_base_station_mode(self, mode): """Set the mode in the base station.""" # Get the list of base stations identified by library # Some Arlo cameras does not have base station # So check if there is base station detected first # if yes, then choose the primary base station # Set the mode on the chosen base station if base_stations := self.hass.data[DATA_ARLO].base_stations: primary_base_station = base_stations[0] primary_base_station.mode = mode def enable_motion_detection(self): """Enable the Motion detection in base station (Arm).""" self._motion_status = True self.set_base_station_mode(ARLO_MODE_ARMED) def disable_motion_detection(self): """Disable the motion detection in base station (Disarm).""" self._motion_status = False self.set_base_station_mode(ARLO_MODE_DISARMED)