""" Support for IP Webcam, an Android app that acts as a full-featured webcam. For more details about this component, please refer to the documentation at https://home-assistant.io/components/android_ip_webcam/ """ import asyncio import logging from datetime import timedelta import voluptuous as vol from homeassistant.core import callback from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, CONF_SCAN_INTERVAL, CONF_PLATFORM) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_send, async_dispatcher_connect) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.components.camera.mjpeg import ( CONF_MJPEG_URL, CONF_STILL_IMAGE_URL) REQUIREMENTS = ['pydroid-ipcam==0.8'] _LOGGER = logging.getLogger(__name__) ATTR_AUD_CONNS = 'Audio Connections' ATTR_HOST = 'host' ATTR_VID_CONNS = 'Video Connections' CONF_MOTION_SENSOR = 'motion_sensor' DATA_IP_WEBCAM = 'android_ip_webcam' DEFAULT_NAME = 'IP Webcam' DEFAULT_PORT = 8080 DEFAULT_TIMEOUT = 10 DOMAIN = 'android_ip_webcam' SCAN_INTERVAL = timedelta(seconds=10) SIGNAL_UPDATE_DATA = 'android_ip_webcam_update' KEY_MAP = { 'audio_connections': 'Audio Connections', 'adet_limit': 'Audio Trigger Limit', 'antibanding': 'Anti-banding', 'audio_only': 'Audio Only', 'battery_level': 'Battery Level', 'battery_temp': 'Battery Temperature', 'battery_voltage': 'Battery Voltage', 'coloreffect': 'Color Effect', 'exposure': 'Exposure Level', 'exposure_lock': 'Exposure Lock', 'ffc': 'Front-facing Camera', 'flashmode': 'Flash Mode', 'focus': 'Focus', 'focus_homing': 'Focus Homing', 'focus_region': 'Focus Region', 'focusmode': 'Focus Mode', 'gps_active': 'GPS Active', 'idle': 'Idle', 'ip_address': 'IPv4 Address', 'ipv6_address': 'IPv6 Address', 'ivideon_streaming': 'Ivideon Streaming', 'light': 'Light Level', 'mirror_flip': 'Mirror Flip', 'motion': 'Motion', 'motion_active': 'Motion Active', 'motion_detect': 'Motion Detection', 'motion_event': 'Motion Event', 'motion_limit': 'Motion Limit', 'night_vision': 'Night Vision', 'night_vision_average': 'Night Vision Average', 'night_vision_gain': 'Night Vision Gain', 'orientation': 'Orientation', 'overlay': 'Overlay', 'photo_size': 'Photo Size', 'pressure': 'Pressure', 'proximity': 'Proximity', 'quality': 'Quality', 'scenemode': 'Scene Mode', 'sound': 'Sound', 'sound_event': 'Sound Event', 'sound_timeout': 'Sound Timeout', 'torch': 'Torch', 'video_connections': 'Video Connections', 'video_chunk_len': 'Video Chunk Length', 'video_recording': 'Video Recording', 'video_size': 'Video Size', 'whitebalance': 'White Balance', 'whitebalance_lock': 'White Balance Lock', 'zoom': 'Zoom' } ICON_MAP = { 'audio_connections': 'mdi:speaker', 'battery_level': 'mdi:battery', 'battery_temp': 'mdi:thermometer', 'battery_voltage': 'mdi:battery-charging-100', 'exposure_lock': 'mdi:camera', 'ffc': 'mdi:camera-front-variant', 'focus': 'mdi:image-filter-center-focus', 'gps_active': 'mdi:crosshairs-gps', 'light': 'mdi:flashlight', 'motion': 'mdi:run', 'night_vision': 'mdi:weather-night', 'overlay': 'mdi:monitor', 'pressure': 'mdi:gauge', 'proximity': 'mdi:map-marker-radius', 'quality': 'mdi:quality-high', 'sound': 'mdi:speaker', 'sound_event': 'mdi:speaker', 'sound_timeout': 'mdi:speaker', 'torch': 'mdi:white-balance-sunny', 'video_chunk_len': 'mdi:video', 'video_connections': 'mdi:eye', 'video_recording': 'mdi:record-rec', 'whitebalance_lock': 'mdi:white-balance-auto' } SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision', 'overlay', 'torch', 'whitebalance_lock', 'video_recording'] SENSORS = ['audio_connections', 'battery_level', 'battery_temp', 'battery_voltage', 'light', 'motion', 'pressure', 'proximity', 'sound', 'video_connections'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [vol.In(SWITCHES)]), vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSORS)]), vol.Optional(CONF_MOTION_SENSOR): cv.boolean, })]) }, extra=vol.ALLOW_EXTRA) @asyncio.coroutine def async_setup(hass, config): """Set up the IP Webcam component.""" from pydroid_ipcam import PyDroidIPCam webcams = hass.data[DATA_IP_WEBCAM] = {} websession = async_get_clientsession(hass) @asyncio.coroutine def async_setup_ipcamera(cam_config): """Set up an IP camera.""" host = cam_config[CONF_HOST] username = cam_config.get(CONF_USERNAME) password = cam_config.get(CONF_PASSWORD) name = cam_config[CONF_NAME] interval = cam_config[CONF_SCAN_INTERVAL] switches = cam_config.get(CONF_SWITCHES) sensors = cam_config.get(CONF_SENSORS) motion = cam_config.get(CONF_MOTION_SENSOR) # Init ip webcam cam = PyDroidIPCam( hass.loop, websession, host, cam_config[CONF_PORT], username=username, password=password, timeout=cam_config[CONF_TIMEOUT] ) if switches is None: switches = [setting for setting in cam.enabled_settings if setting in SWITCHES] if sensors is None: sensors = [sensor for sensor in cam.enabled_sensors if sensor in SENSORS] sensors.extend(['audio_connections', 'video_connections']) if motion is None: motion = 'motion_active' in cam.enabled_sensors @asyncio.coroutine def async_update_data(now): """Update data from IP camera in SCAN_INTERVAL.""" yield from cam.update() async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) async_track_point_in_utc_time( hass, async_update_data, utcnow() + interval) yield from async_update_data(None) # Load platforms webcams[host] = cam mjpeg_camera = { CONF_PLATFORM: 'mjpeg', CONF_MJPEG_URL: cam.mjpeg_url, CONF_STILL_IMAGE_URL: cam.image_url, CONF_NAME: name, } if username and password: mjpeg_camera.update({ CONF_USERNAME: username, CONF_PASSWORD: password }) hass.async_create_task(discovery.async_load_platform( hass, 'camera', 'mjpeg', mjpeg_camera, config)) if sensors: hass.async_create_task(discovery.async_load_platform( hass, 'sensor', DOMAIN, { CONF_NAME: name, CONF_HOST: host, CONF_SENSORS: sensors, }, config)) if switches: hass.async_create_task(discovery.async_load_platform( hass, 'switch', DOMAIN, { CONF_NAME: name, CONF_HOST: host, CONF_SWITCHES: switches, }, config)) if motion: hass.async_create_task(discovery.async_load_platform( hass, 'binary_sensor', DOMAIN, { CONF_HOST: host, CONF_NAME: name, }, config)) tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]] if tasks: yield from asyncio.wait(tasks, loop=hass.loop) return True class AndroidIPCamEntity(Entity): """The Android device running IP Webcam.""" def __init__(self, host, ipcam): """Initialize the data object.""" self._host = host self._ipcam = ipcam @asyncio.coroutine def async_added_to_hass(self): """Register update dispatcher.""" @callback def async_ipcam_update(host): """Update callback.""" if self._host != host: return self.async_schedule_update_ha_state(True) async_dispatcher_connect( self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update) @property def should_poll(self): """Return True if entity has to be polled for state.""" return False @property def available(self): """Return True if entity is available.""" return self._ipcam.available @property def device_state_attributes(self): """Return the state attributes.""" state_attr = {ATTR_HOST: self._host} if self._ipcam.status_data is None: return state_attr state_attr[ATTR_VID_CONNS] = \ self._ipcam.status_data.get('video_connections') state_attr[ATTR_AUD_CONNS] = \ self._ipcam.status_data.get('audio_connections') return state_attr