From 93118fcadeeeb2a7c5adcdc4bc0ad9f930abfd75 Mon Sep 17 00:00:00 2001 From: pvizeli <pvizeli@syshack.ch> Date: Wed, 8 Mar 2017 16:46:41 +0100 Subject: [PATCH] Android IP Cam support --- homeassistant/components/android_ip_webcam.py | 265 ++++++++++++++++++ .../binary_sensor/android_ip_webcam.py | 65 +++++ .../components/sensor/android_ip_webcam.py | 93 ++++++ .../components/switch/android_ip_webcam.py | 95 +++++++ requirements_all.txt | 3 + 5 files changed, 521 insertions(+) create mode 100644 homeassistant/components/android_ip_webcam.py create mode 100644 homeassistant/components/binary_sensor/android_ip_webcam.py create mode 100644 homeassistant/components/sensor/android_ip_webcam.py create mode 100644 homeassistant/components/switch/android_ip_webcam.py diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py new file mode 100644 index 00000000000..be318c7c558 --- /dev/null +++ b/homeassistant/components/android_ip_webcam.py @@ -0,0 +1,265 @@ +""" +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 +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) +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 + +DOMAIN = 'android_ip_webcam' +REQUIREMENTS = ["pydroid-ipcam==0.1"] + +SCAN_INTERVAL = timedelta(seconds=10) + +DATA_IP_WEBCAM = 'android_ip_webcam' + +ATTR_VID_CONNS = 'Video Connections' +ATTR_AUD_CONNS = 'Audio Connections' + +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'] + +SIGNAL_UPDATE_DATA = 'android_ip_webcam_update' + +CONF_MOTION_SENSOR = 'motion_sensor' + +DEFAULT_MOTION_SENSOR = True +DEFAULT_NAME = 'IP Webcam' +DEFAULT_PORT = 8080 +DEFAULT_TIMEOUT = 10 + + +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, default=SWITCHES): + vol.All(cv.ensure_list, [vol.In(SWITCHES)]), + vol.Optional(CONF_SENSORS, default=SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSORS)]), + vol.Optional(CONF_MOTION_SENSOR, default=DEFAULT_MOTION_SENSOR): bool + })]) +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Setup 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): + """Setup a 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[SCAN_INTERVAL] + + cam = PyDroidIPCam( + hass.loop, websession, host, cam_config[CONF_PORT], + username=username, password=password, + timeout=cam_config[CONF_TIMEOUT] + ) + + @asyncio.coroutine + def async_update_data(now): + """Update data from ipcam in SCAN_INTERVAL.""" + yield from cam.update() + async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) + + async_track_point_in_utc_time( + hass, utcnow() + interval, async_update_data) + + yield from async_update_data(None) + webcams[host] = cam + + mjpeg_camera = { + 'mjpeg_url': cam.mjpeg_url, + 'still_image_url': cam.image_url, + CONF_NAME: name, + } + if username and password: + mjpeg_camera.update({ + CONF_USERNAME: username, + CONF_PASSWORD: password + }) + + if cam_config[CONF_MOTION_SENSOR]: + hass.async_add_job(discovery.async_load_platform( + hass, 'binary_sensor', DOMAIN, { + CONF_HOST: host, + CONF_NAME: name, + }, config)) + + hass.async_add_job(discovery.async_load_platform( + hass, 'camera', 'mjpeg', mjpeg_camera, config)) + + hass.async_add_job(discovery.async_load_platform( + hass, 'sensor', DOMAIN, { + CONF_NAME: name, + CONF_HOST: host, + CONF_SENSORS: cam_config[CONF_SENSORS], + }, config)) + + hass.async_add_job(discovery.async_load_platform( + hass, 'switch', DOMAIN, { + CONF_NAME: name, + CONF_HOST: host, + CONF_SWITCHES: cam_config[CONF_SWITCHES], + }, 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 oject.""" + 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.hass.async_add_job(self.async_update_ha_state(True)) + + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update) + + @property + def should_poll(self): + """Is update over central callback.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + state_attr = {} + if self._ipcam.status_data is not 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') + state_attr.update(self._ipcam.current_settings) + + return state_attr diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py new file mode 100644 index 00000000000..2feb21f2ec5 --- /dev/null +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -0,0 +1,65 @@ +""" +Support for IP Webcam binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.android_ip_webcam/ +""" +import asyncio + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.android_ip_webcam import ( + KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME) + +DEPENDENCIES = ['android_ip_webcam'] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup IP Webcam binary sensors.""" + if discovery_info is None: + return + + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + ipcam = hass.data[DATA_IP_WEBCAM][host] + + async_add_devices( + [IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True) + + +class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): + """Represents an IP Webcam binary sensor.""" + + def __init__(self, name, host, ipcam, sensor): + """Initialize the binary sensor.""" + super().__init__(host, ipcam) + + self._sensor = sensor + self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) + self._name = '{} {}'.format(name, self._mapped_name) + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the binary sensor, if any.""" + return self._name + + @property + def is_on(self): + """True if the binary sensor is on.""" + return self._state + + def update(self): + """Retrieve latest state.""" + if self._ipcam.status_data not None: + return + + container = self._ipcam.sensor_data.get(self._sensor) + data_point = container.get('data', [[0, [0.0]]]) + self._state = data_point[0][-1][0] == 1.0 + + @property + def sensor_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'motion' diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/sensor/android_ip_webcam.py new file mode 100644 index 00000000000..f0151758b48 --- /dev/null +++ b/homeassistant/components/sensor/android_ip_webcam.py @@ -0,0 +1,93 @@ +""" +Support for IP Webcam sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.android_ip_webcam/ +""" +import asyncio + +from homeassistant.components.android_ip_webcam import ( + KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, + CONF_NAME, CONF_SENSORS) + +DEPENDENCIES = ['android_ip_webcam'] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup the IP Webcam Sensor.""" + if discovery_info is None: + return + + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + sensors = discovery_info[CONF_SENSORS] + ipcam = hass.data[DATA_IP_WEBCAM][host] + + all_sensors = [] + + for sensor in sensors: + all_sensors.append(IPWebcamSensor(name, host, ipcam, sensor)) + + async_add_devices(all_sensors, True) + + +class IPWebcamSensor(AndroidIPCamEntity): + """Representation of a IP Webcam sensor.""" + + def __init__(self, name, host, ipcam, sensor): + """Initialize the sensor.""" + super().__init__(host, ipcam) + + self._sensor = sensor + self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) + self._name = '{} {}'.format(name, self._mapped_name) + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the sensor, if any.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @asyncio.coroutine + def async_update(self): + """Retrieve latest state.""" + if self._ipcam.status_data is None or self._ipcam.sensor_data is None: + return + + if self._sensor in ('audio_connections', 'video_connections'): + self._state = self._ipcam.status_data.get(self._sensor) + self._unit = 'Connections' + else: + container = self._ipcam.sensor_data.get(self._sensor) + self._unit = container.get('unit', self._unit) + data_point = container.get('data', [[0, [0.0]]]) + if data_point and data_point[0]: + self._state = data_point[0][-1][0] + + @property + def icon(self): + """Return the icon for the sensor.""" + if self._sensor == 'battery_level' and self._state is not None: + rounded_level = round(int(self._state), -1) + returning_icon = 'mdi:battery' + if rounded_level < 10: + returning_icon = 'mdi:battery-outline' + elif self._state == 100: + returning_icon = 'mdi:battery' + else: + returning_icon = 'mdi:battery-{}'.format(str(rounded_level)) + + return returning_icon + return ICON_MAP.get(self._sensor, 'mdi:eye') diff --git a/homeassistant/components/switch/android_ip_webcam.py b/homeassistant/components/switch/android_ip_webcam.py new file mode 100644 index 00000000000..04d11be93fa --- /dev/null +++ b/homeassistant/components/switch/android_ip_webcam.py @@ -0,0 +1,95 @@ +""" +Support for IP Webcam settings. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.android_ip_webcam/ +""" +import asyncio + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.android_ip_webcam import ( + KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, + CONF_NAME, CONF_SWITCHES) + +DEPENDENCIES = ['android_ip_webcam'] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the IP Webcam switch platform.""" + if discovery_info is None: + return + + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + switches = discovery_info[CONF_SWITCHES] + ipcam = hass.data[DATA_IP_WEBCAM][host] + + all_switches = [] + + for setting in switches: + all_switches.append(IPWebcamSettingsSwitch(name, host, ipcam, setting)) + + async_add_devices(all_switches, True) + + +class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): + """An abstract class for an IP Webcam setting.""" + + def __init__(self, name, host, ipcam, setting): + """Initialize the settings switch.""" + super().__init__(host, ipcam) + + self._setting = setting + self._mapped_name = KEY_MAP.get(self._setting, self._setting) + self._name = '{} {}'.format(self._device.name, self._mapped_name) + self._state = False + + @property + def name(self): + """Return the the name of the node.""" + return self._name + + @asyncio.coroutine + def async_update(self): + """Get the updated status of the switch.""" + if self._ipcam.status_data is not None: + self._state = self._ipcam.current_settings.get(self._setting) + + @property + def is_on(self): + """Return the boolean response if the node is on.""" + return self._state + + @asyncio.coroutine + def async_turn_on(self, **kwargs): + """Turn device on.""" + if self._setting is 'torch': + yield from self._ipcam.torch(activate=True) + elif self._setting is 'focus': + yield from self._ipcam.focus(activate=True) + elif self._setting is 'video_recording': + yield from self._ipcam.record(record=True) + else: + yield from self._ipcam.change_setting(self._setting, True) + self._state = True + self.hass.async_add_job(self.async_update_ha_state()) + + @asyncio.coroutine + def async_turn_off(self, **kwargs): + """Turn device off.""" + if self._setting is 'torch': + yield from self._ipcam.torch(activate=False) + elif self._setting is 'focus': + yield from self._ipcam.focus(activate=False) + elif self._setting is 'video_recording': + yield from self._ipcam.record(record=False) + else: + yield from self._ipcam.change_setting(self._setting, False) + self._state = False + self.hass.async_add_job(self.async_update_ha_state()) + + @property + def icon(self): + """Return the icon for the switch.""" + return ICON_MAP.get(self._setting, 'mdi:flash') diff --git a/requirements_all.txt b/requirements_all.txt index 6e4f3912ab2..886fb65e936 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -481,6 +481,9 @@ pycmus==0.1.0 # homeassistant.components.zwave pydispatcher==2.0.5 +# homeassistant.components.android_ip_webcam +pydroid-ipcam==0.1 + # homeassistant.components.sensor.ebox pyebox==0.1.0 -- GitLab