"""This component provides basic support for Foscam IP cameras.""" import logging import asyncio from libpyfoscam import FoscamCamera import voluptuous as vol from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM from homeassistant.const import ( CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, ATTR_ENTITY_ID, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_extract_entity_ids from .const import DOMAIN as FOSCAM_DOMAIN from .const import DATA as FOSCAM_DATA from .const import ENTITIES as FOSCAM_ENTITIES _LOGGER = logging.getLogger(__name__) CONF_IP = "ip" CONF_RTSP_PORT = "rtsp_port" DEFAULT_NAME = "Foscam Camera" DEFAULT_PORT = 88 SERVICE_PTZ = "ptz" ATTR_MOVEMENT = "movement" ATTR_TRAVELTIME = "travel_time" DEFAULT_TRAVELTIME = 0.125 DIR_UP = "up" DIR_DOWN = "down" DIR_LEFT = "left" DIR_RIGHT = "right" DIR_TOPLEFT = "top_left" DIR_TOPRIGHT = "top_right" DIR_BOTTOMLEFT = "bottom_left" DIR_BOTTOMRIGHT = "bottom_right" MOVEMENT_ATTRS = { DIR_UP: "ptz_move_up", DIR_DOWN: "ptz_move_down", DIR_LEFT: "ptz_move_left", DIR_RIGHT: "ptz_move_right", DIR_TOPLEFT: "ptz_move_top_left", DIR_TOPRIGHT: "ptz_move_top_right", DIR_BOTTOMLEFT: "ptz_move_bottom_left", DIR_BOTTOMRIGHT: "ptz_move_bottom_right", } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_IP): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_RTSP_PORT): cv.port, } ) SERVICE_PTZ_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_MOVEMENT): vol.In( [ DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT, DIR_TOPLEFT, DIR_TOPRIGHT, DIR_BOTTOMLEFT, DIR_BOTTOMRIGHT, ] ), vol.Optional(ATTR_TRAVELTIME, default=DEFAULT_TRAVELTIME): cv.small_float, } ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a Foscam IP Camera.""" async def async_handle_ptz(service): """Handle PTZ service call.""" movement = service.data[ATTR_MOVEMENT] travel_time = service.data[ATTR_TRAVELTIME] entity_ids = await async_extract_entity_ids(hass, service) if not entity_ids: return _LOGGER.debug("Moving '%s' camera(s): %s", movement, entity_ids) all_cameras = hass.data[FOSCAM_DATA][FOSCAM_ENTITIES] target_cameras = [ camera for camera in all_cameras if camera.entity_id in entity_ids ] for camera in target_cameras: await camera.async_perform_ptz(movement, travel_time) hass.services.async_register( FOSCAM_DOMAIN, SERVICE_PTZ, async_handle_ptz, schema=SERVICE_PTZ_SCHEMA ) camera = FoscamCamera( config[CONF_IP], config[CONF_PORT], config[CONF_USERNAME], config[CONF_PASSWORD], verbose=False, ) rtsp_port = config.get(CONF_RTSP_PORT) if not rtsp_port: ret, response = await hass.async_add_executor_job(camera.get_port_info) if ret == 0: rtsp_port = response.get("rtspPort") or response.get("mediaPort") ret, response = await hass.async_add_executor_job(camera.get_motion_detect_config) motion_status = False if ret != 0 and response == 1: motion_status = True async_add_entities( [ HassFoscamCamera( camera, config[CONF_NAME], config[CONF_USERNAME], config[CONF_PASSWORD], rtsp_port, motion_status, ) ] ) class HassFoscamCamera(Camera): """An implementation of a Foscam IP camera.""" def __init__(self, camera, name, username, password, rtsp_port, motion_status): """Initialize a Foscam camera.""" super().__init__() self._foscam_session = camera self._name = name self._username = username self._password = password self._rtsp_port = rtsp_port self._motion_status = motion_status async def async_added_to_hass(self): """Handle entity addition to hass.""" entities = self.hass.data.setdefault(FOSCAM_DATA, {}).setdefault( FOSCAM_ENTITIES, [] ) entities.append(self) def camera_image(self): """Return a still image response from the camera.""" # Send the request to snap a picture and return raw jpg data # Handle exception if host is not reachable or url failed result, response = self._foscam_session.snap_picture_2() if result != 0: return None return response @property def supported_features(self): """Return supported features.""" if self._rtsp_port: return SUPPORT_STREAM return 0 async def stream_source(self): """Return the stream source.""" if self._rtsp_port: return "rtsp://{}:{}@{}:{}/videoMain".format( self._username, self._password, self._foscam_session.host, self._rtsp_port, ) return None @property def motion_detection_enabled(self): """Camera Motion Detection Status.""" return self._motion_status def enable_motion_detection(self): """Enable motion detection in camera.""" try: ret = self._foscam_session.enable_motion_detection() if ret != 0: return self._motion_status = True except TypeError: _LOGGER.debug("Communication problem") def disable_motion_detection(self): """Disable motion detection.""" try: ret = self._foscam_session.disable_motion_detection() if ret != 0: return self._motion_status = False except TypeError: _LOGGER.debug("Communication problem") async def async_perform_ptz(self, movement, travel_time): """Perform a PTZ action on the camera.""" _LOGGER.debug("PTZ action '%s' on %s", movement, self._name) movement_function = getattr(self._foscam_session, MOVEMENT_ATTRS[movement]) ret, _ = await self.hass.async_add_executor_job(movement_function) if ret != 0: _LOGGER.error("Error moving %s '%s': %s", movement, self._name, ret) return await asyncio.sleep(travel_time) ret, _ = await self.hass.async_add_executor_job( self._foscam_session.ptz_stop_run ) if ret != 0: _LOGGER.error("Error stopping movement on '%s': %s", self._name, ret) return @property def name(self): """Return the name of this camera.""" return self._name