From 93118fcadeeeb2a7c5adcdc4bc0ad9f930abfd75 Mon Sep 17 00:00:00 2001 From: pvizeli Date: Wed, 8 Mar 2017 16:46:41 +0100 Subject: [PATCH 1/8] 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 From 21feff5fd859048de20c089c2636daf70d3b7d6e Mon Sep 17 00:00:00 2001 From: pvizeli Date: Wed, 8 Mar 2017 17:52:49 +0100 Subject: [PATCH 2/8] add available --- homeassistant/components/android_ip_webcam.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index be318c7c558..d90232a87d0 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -249,6 +249,11 @@ class AndroidIPCamEntity(Entity): """Is update over central callback.""" 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.""" From b1736994b72a90ff18d7461b0afe00dd93ba2cdc Mon Sep 17 00:00:00 2001 From: pvizeli Date: Wed, 8 Mar 2017 17:57:22 +0100 Subject: [PATCH 3/8] fix lint --- homeassistant/components/android_ip_webcam.py | 2 +- homeassistant/components/binary_sensor/android_ip_webcam.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index d90232a87d0..b51045be3d9 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -258,7 +258,7 @@ class AndroidIPCamEntity(Entity): def device_state_attributes(self): """Return the state attributes.""" state_attr = {} - if self._ipcam.status_data is not None: + if self._ipcam.status_data is None: return state_attr state_attr[ATTR_VID_CONNS] = \ diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py index 2feb21f2ec5..ff193316439 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -50,9 +50,10 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): """True if the binary sensor is on.""" return self._state - def update(self): + @asyncio.coroutine + def async_update(self): """Retrieve latest state.""" - if self._ipcam.status_data not None: + if self._ipcam.status_data is None: return container = self._ipcam.sensor_data.get(self._sensor) From 185ccc4fc4bc2e62391677126fbfb8a66c07226b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 9 Mar 2017 01:00:57 +0100 Subject: [PATCH 4/8] Fix some things --- homeassistant/components/android_ip_webcam.py | 9 +++++---- .../components/binary_sensor/android_ip_webcam.py | 8 +++++++- homeassistant/components/camera/mjpeg.py | 2 ++ homeassistant/components/sensor/android_ip_webcam.py | 6 ++++++ homeassistant/components/switch/android_ip_webcam.py | 5 ++++- requirements_all.txt | 2 +- 6 files changed, 25 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index b51045be3d9..3b6de5c4892 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -23,7 +23,7 @@ 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"] +REQUIREMENTS = ["pydroid-ipcam==0.2"] SCAN_INTERVAL = timedelta(seconds=10) @@ -161,7 +161,7 @@ def async_setup(hass, config): username = cam_config.get(CONF_USERNAME) password = cam_config.get(CONF_PASSWORD) name = cam_config[CONF_NAME] - interval = cam_config[SCAN_INTERVAL] + interval = cam_config[CONF_SCAN_INTERVAL] cam = PyDroidIPCam( hass.loop, websession, host, cam_config[CONF_PORT], @@ -176,12 +176,13 @@ def async_setup(hass, config): async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) async_track_point_in_utc_time( - hass, utcnow() + interval, async_update_data) + hass, async_update_data, utcnow() + interval) yield from async_update_data(None) webcams[host] = cam mjpeg_camera = { + 'platform': 'mjpeg', 'mjpeg_url': cam.mjpeg_url, 'still_image_url': cam.image_url, CONF_NAME: name, @@ -246,7 +247,7 @@ class AndroidIPCamEntity(Entity): @property def should_poll(self): - """Is update over central callback.""" + """Return True if entity has to be polled for state.""" return False @property diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py index ff193316439..8c0699040ed 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/binary_sensor.android_ip_webcam/ """ import asyncio +from homeassistant.const import STATE_UNKNOWN from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.android_ip_webcam import ( KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME) @@ -54,6 +55,11 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): def async_update(self): """Retrieve latest state.""" if self._ipcam.status_data is None: + self._state = STATE_UNKNOWN + return + + if self._sensor not in self._ipcam.enabled_sensors: + self._state = STATE_UNKNOWN return container = self._ipcam.sensor_data.get(self._sensor) @@ -61,6 +67,6 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): self._state = data_point[0][-1][0] == 1.0 @property - def sensor_class(self): + def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" return 'motion' diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index fa46ea55e2c..532b91e7442 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -45,6 +45,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a MJPEG IP Camera.""" + if discovery_info: + config = PLATFORM_SCHEMA(discovery_info) async_add_devices([MjpegCamera(hass, config)]) diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/sensor/android_ip_webcam.py index f0151758b48..14a77da632e 100644 --- a/homeassistant/components/sensor/android_ip_webcam.py +++ b/homeassistant/components/sensor/android_ip_webcam.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/sensor.android_ip_webcam/ """ import asyncio +from homeassistant.const import STATE_UNKNOWN from homeassistant.components.android_ip_webcam import ( KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME, CONF_SENSORS) @@ -64,6 +65,11 @@ class IPWebcamSensor(AndroidIPCamEntity): def async_update(self): """Retrieve latest state.""" if self._ipcam.status_data is None or self._ipcam.sensor_data is None: + self._state = STATE_UNKNOWN + return + + if self._sensor not in self._ipcam.enabled_sensors: + self._state = STATE_UNKNOWN return if self._sensor in ('audio_connections', 'video_connections'): diff --git a/homeassistant/components/switch/android_ip_webcam.py b/homeassistant/components/switch/android_ip_webcam.py index 04d11be93fa..6362e0c5261 100644 --- a/homeassistant/components/switch/android_ip_webcam.py +++ b/homeassistant/components/switch/android_ip_webcam.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/switch.android_ip_webcam/ """ import asyncio +from homeassistant.const import STATE_UNKNOWN from homeassistant.components.switch import SwitchDevice from homeassistant.components.android_ip_webcam import ( KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, @@ -42,7 +43,7 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): self._setting = setting self._mapped_name = KEY_MAP.get(self._setting, self._setting) - self._name = '{} {}'.format(self._device.name, self._mapped_name) + self._name = '{} {}'.format(name, self._mapped_name) self._state = False @property @@ -55,6 +56,8 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): """Get the updated status of the switch.""" if self._ipcam.status_data is not None: self._state = self._ipcam.current_settings.get(self._setting) + else: + self._state = STATE_UNKNOWN @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 886fb65e936..cba53d3a13d 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -482,7 +482,7 @@ pycmus==0.1.0 pydispatcher==2.0.5 # homeassistant.components.android_ip_webcam -pydroid-ipcam==0.1 +pydroid-ipcam==0.2 # homeassistant.components.sensor.ebox pyebox==0.1.0 From 21f3b62d094789d81e3b9254ab3274b55c1a95c8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 9 Mar 2017 01:03:15 +0100 Subject: [PATCH 5/8] fix coverage --- .coveragerc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index cd9ca93b5c8..6b924e9767b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -14,9 +14,12 @@ omit = homeassistant/components/arduino.py homeassistant/components/*/arduino.py + homeassistant/components/android_ip_webcam.py + homeassistant/components/*/android_ip_webcam.py + homeassistant/components/bbb_gpio.py homeassistant/components/*/bbb_gpio.py - + homeassistant/components/blink.py homeassistant/components/*/blink.py From bcd4def0aea5c41559d566a38f157dffafd9c651 Mon Sep 17 00:00:00 2001 From: pvizeli Date: Thu, 9 Mar 2017 12:00:50 +0100 Subject: [PATCH 6/8] pump version 0.3 / make a lot of improvments --- homeassistant/components/android_ip_webcam.py | 68 +++++++++++++------ .../binary_sensor/android_ip_webcam.py | 14 +--- .../components/sensor/android_ip_webcam.py | 15 +--- .../components/switch/android_ip_webcam.py | 6 +- requirements_all.txt | 2 +- 5 files changed, 53 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 3b6de5c4892..e236a04a40a 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -12,7 +12,8 @@ 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_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 @@ -21,14 +22,17 @@ from homeassistant.helpers.dispatcher import ( 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, DOMAIN as MJPEG_DOMAIN) DOMAIN = 'android_ip_webcam' -REQUIREMENTS = ["pydroid-ipcam==0.2"] +REQUIREMENTS = ["pydroid-ipcam==0.3"] SCAN_INTERVAL = timedelta(seconds=10) DATA_IP_WEBCAM = 'android_ip_webcam' +ATTR_HOST = 'host' ATTR_VID_CONNS = 'Video Connections' ATTR_AUD_CONNS = 'Audio Connections' @@ -119,9 +123,11 @@ SENSORS = ['audio_connections', 'battery_level', 'battery_temp', SIGNAL_UPDATE_DATA = 'android_ip_webcam_update' +CONF_AUTO_DISCOVERY = 'auto_discovery' CONF_MOTION_SENSOR = 'motion_sensor' -DEFAULT_MOTION_SENSOR = True +DEFAULT_AUTO_DISCOVERY = True +DEFAULT_MOTION_SENSOR = False DEFAULT_NAME = 'IP Webcam' DEFAULT_PORT = 8080 DEFAULT_TIMEOUT = 10 @@ -137,11 +143,14 @@ CONFIG_SCHEMA = vol.Schema({ cv.time_period, vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, - vol.Optional(CONF_SWITCHES, default=SWITCHES): + vol.Optional(CONF_AUTO_DISCOVERY, default=DEFAULT_AUTO_DISCOVERY): + cv.boolean, + vol.Optional(CONF_SWITCHES, default=[]): vol.All(cv.ensure_list, [vol.In(SWITCHES)]), - vol.Optional(CONF_SENSORS, default=SENSORS): + vol.Optional(CONF_SENSORS, default=[]): vol.All(cv.ensure_list, [vol.In(SENSORS)]), - vol.Optional(CONF_MOTION_SENSOR, default=DEFAULT_MOTION_SENSOR): bool + vol.Optional(CONF_MOTION_SENSOR, default=DEFAULT_MOTION_SENSOR): + cv.boolean, })]) }, extra=vol.ALLOW_EXTRA) @@ -162,7 +171,11 @@ def async_setup(hass, config): password = cam_config.get(CONF_PASSWORD) name = cam_config[CONF_NAME] interval = cam_config[CONF_SCAN_INTERVAL] + switches = cam_config[CONF_SWITCHES] + sensors = cam_config[CONF_SENSORS] + motion = cam_config[CONF_MOTION_SENSOR] + # init ip webcam cam = PyDroidIPCam( hass.loop, websession, host, cam_config[CONF_PORT], username=username, password=password, @@ -179,12 +192,27 @@ def async_setup(hass, config): hass, async_update_data, utcnow() + interval) yield from async_update_data(None) + + # use autodiscovery to detect sensors/configs + if cam_config[CONF_AUTO_DISCOVERY]: + if not cam.available: + _LOGGER.error( + "Android webcam %s not found for discovery!", host) + return + + sensors = cam.enabled_sensors + switches = cam.enabled_settings + if 'motion_active' in sensors: + sensors.pop('motion_active') + motion = True + + # load platforms webcams[host] = cam mjpeg_camera = { - 'platform': 'mjpeg', - 'mjpeg_url': cam.mjpeg_url, - 'still_image_url': cam.image_url, + CONF_PLATFORM: MJPEG_DOMAIN, + CONF_MJPEG_URL: cam.mjpeg_url, + CONF_STILL_IMAGE_URL: cam.image_url, CONF_NAME: name, } if username and password: @@ -193,13 +221,6 @@ def async_setup(hass, config): 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)) @@ -207,16 +228,24 @@ def async_setup(hass, config): hass, 'sensor', DOMAIN, { CONF_NAME: name, CONF_HOST: host, - CONF_SENSORS: cam_config[CONF_SENSORS], + CONF_SENSORS: 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], + CONF_SWITCHES: switches, }, config)) + if motion: + hass.async_add_job(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) @@ -258,7 +287,7 @@ class AndroidIPCamEntity(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - state_attr = {} + state_attr = {ATTTR_HOST: self._host} if self._ipcam.status_data is None: return state_attr @@ -266,6 +295,5 @@ class AndroidIPCamEntity(Entity): 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 index 8c0699040ed..4b9b4af24af 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/binary_sensor.android_ip_webcam/ """ import asyncio -from homeassistant.const import STATE_UNKNOWN from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.android_ip_webcam import ( KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME) @@ -54,17 +53,8 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): @asyncio.coroutine def async_update(self): """Retrieve latest state.""" - if self._ipcam.status_data is None: - self._state = STATE_UNKNOWN - return - - if self._sensor not in self._ipcam.enabled_sensors: - self._state = STATE_UNKNOWN - 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 + state, _ = self._ipcam.export_sensor(self._sensor) + self._state = state == 1.0 @property def device_class(self): diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/sensor/android_ip_webcam.py index 14a77da632e..5b7099d1c96 100644 --- a/homeassistant/components/sensor/android_ip_webcam.py +++ b/homeassistant/components/sensor/android_ip_webcam.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/sensor.android_ip_webcam/ """ import asyncio -from homeassistant.const import STATE_UNKNOWN from homeassistant.components.android_ip_webcam import ( KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME, CONF_SENSORS) @@ -64,23 +63,11 @@ class IPWebcamSensor(AndroidIPCamEntity): @asyncio.coroutine def async_update(self): """Retrieve latest state.""" - if self._ipcam.status_data is None or self._ipcam.sensor_data is None: - self._state = STATE_UNKNOWN - return - - if self._sensor not in self._ipcam.enabled_sensors: - self._state = STATE_UNKNOWN - 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] + self._state, self._unit = self._ipcam.export_sensor(self._sensor) @property def icon(self): diff --git a/homeassistant/components/switch/android_ip_webcam.py b/homeassistant/components/switch/android_ip_webcam.py index 6362e0c5261..6e3b9ed77f1 100644 --- a/homeassistant/components/switch/android_ip_webcam.py +++ b/homeassistant/components/switch/android_ip_webcam.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/switch.android_ip_webcam/ """ import asyncio -from homeassistant.const import STATE_UNKNOWN from homeassistant.components.switch import SwitchDevice from homeassistant.components.android_ip_webcam import ( KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, @@ -54,10 +53,7 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): @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) - else: - self._state = STATE_UNKNOWN + self._state = bool(self._ipcam.current_settings.get(self._setting)) @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index cba53d3a13d..afa3c2d7126 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -482,7 +482,7 @@ pycmus==0.1.0 pydispatcher==2.0.5 # homeassistant.components.android_ip_webcam -pydroid-ipcam==0.2 +pydroid-ipcam==0.3 # homeassistant.components.sensor.ebox pyebox==0.1.0 From bbcfb9158ad2c5963952888fa3833c43d9768861 Mon Sep 17 00:00:00 2001 From: pvizeli Date: Thu, 9 Mar 2017 12:03:08 +0100 Subject: [PATCH 7/8] fix lint --- homeassistant/components/android_ip_webcam.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index e236a04a40a..fd0320982f9 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -5,6 +5,7 @@ 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 @@ -28,6 +29,7 @@ from homeassistant.components.camera.mjpeg import ( DOMAIN = 'android_ip_webcam' REQUIREMENTS = ["pydroid-ipcam==0.3"] +_LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) DATA_IP_WEBCAM = 'android_ip_webcam' @@ -245,7 +247,6 @@ def async_setup(hass, config): CONF_NAME: name, }, config)) - tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]] if tasks: yield from asyncio.wait(tasks, loop=hass.loop) @@ -287,7 +288,7 @@ class AndroidIPCamEntity(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - state_attr = {ATTTR_HOST: self._host} + state_attr = {ATTR_HOST: self._host} if self._ipcam.status_data is None: return state_attr From 8bbf13ef9fb3bfa0a8f7bd14e34ddb3bbc66056a Mon Sep 17 00:00:00 2001 From: pvizeli Date: Thu, 9 Mar 2017 15:10:39 +0100 Subject: [PATCH 8/8] Fix lint --- homeassistant/components/android_ip_webcam.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index fd0320982f9..31ce2923f58 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -24,7 +24,7 @@ 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, DOMAIN as MJPEG_DOMAIN) + CONF_MJPEG_URL, CONF_STILL_IMAGE_URL) DOMAIN = 'android_ip_webcam' REQUIREMENTS = ["pydroid-ipcam==0.3"] @@ -212,7 +212,7 @@ def async_setup(hass, config): webcams[host] = cam mjpeg_camera = { - CONF_PLATFORM: MJPEG_DOMAIN, + CONF_PLATFORM: 'mjpeg', CONF_MJPEG_URL: cam.mjpeg_url, CONF_STILL_IMAGE_URL: cam.image_url, CONF_NAME: name,