From c157e9918dfae0ca2f3d20fc28d2d5e4d7b76810 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 27 Jan 2017 16:39:54 -0800 Subject: [PATCH 01/23] Initial work on IP Webcam for Android --- .coveragerc | 3 + homeassistant/components/android_ip_webcam.py | 246 ++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 homeassistant/components/android_ip_webcam.py diff --git a/.coveragerc b/.coveragerc index 9a1a31257404d..26ccff9accb05 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,9 @@ omit = homeassistant/helpers/signal.py # omit pieces of code that rely on external devices being present + homeassistant/components/android_ip_webcam.py + homeassistant/components/*/android_ip_webcam.py + homeassistant/components/apcupsd.py homeassistant/components/*/apcupsd.py diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py new file mode 100644 index 0000000000000..b8402735e9203 --- /dev/null +++ b/homeassistant/components/android_ip_webcam.py @@ -0,0 +1,246 @@ +""" +Support for IP Webcam, an Android app that turns a device into a webcam. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/android_ip_webcam/ +""" +import logging +from datetime import datetime +import xml.etree.ElementTree as ET +import requests + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util +from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT, + CONF_USERNAME, CONF_PASSWORD, + CONF_BINARY_SENSORS, CONF_SENSORS, + CONF_SWITCHES, CONF_MONITORED_CONDITIONS) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'android_ip_webcam' + +DATA_IP_WEBCAM = 'android_ip_webcam' + +DEFAULT_NAME = 'IP Webcam' + +DEFAULT_PORT = 8080 +DEFAULT_TIMEOUT = 10 + +ATTR_VID_CONNS = 'Video Connections' +ATTR_AUD_CONNS = 'Audio Connections' + +STATUS_KEY_MAP = { + 'adet_limit': 'Audio Trigger Limit', + 'antibanding': 'Anti-banding', + 'audio_only': 'Audio Only', + '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': 'IP Address', + 'ivideon_streaming': 'Ivideon Streaming', + 'mirror_flip': 'Mirror Flip', + 'motion_detect': 'Motion Detection', + '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', + 'quality': 'Quality', + 'scenemode': 'Scene Mode', + 'torch': 'Torch', + 'video_chunk_len': 'Video Chunk Length', + 'video_recording': 'Video Recording', + 'video_size': 'Video Size', + 'whitebalance': 'White Balance', + 'whitebalance_lock': 'White Balance Lock', + 'zoom': 'Zoom' +} + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list) +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: 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.Inclusive(CONF_USERNAME, 'authentication'): cv.string, + vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, + vol.Optional(CONF_SWITCHES): SENSOR_SCHEMA, + vol.Optional(CONF_SENSORS): SENSOR_SCHEMA, + vol.Optional(CONF_BINARY_SENSORS): SENSOR_SCHEMA + }) +}, extra=vol.ALLOW_EXTRA) + +ALLOWED_ORIENTATIONS = ['landscape', 'upsidedown', 'portrait', + 'upsidedown_portrait'] + + +def setup(hass, config): + """Setup the IP Webcam component.""" + conf = config[DOMAIN] + host = conf[CONF_HOST] + hass.data[DATA_IP_WEBCAM][host] = {'status': {}, 'sensors': {}} + + binary_sensor_config = conf.get(CONF_BINARY_SENSORS, {}) + discovery.load_platform(hass, 'binary_sensor', DOMAIN, + binary_sensor_config, config) + + discovery.load_platform(hass, 'camera', DOMAIN, {}, config) + + sensor_config = conf.get(CONF_SENSORS, {}) + discovery.load_platform(hass, 'sensor', DOMAIN, sensor_config, config) + + switch_config = conf.get(CONF_SWITCHES, {}) + discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) + + return True + + +class IPWebcam(Entity): + """The Android device running IP Webcam.""" + + def __init__(self, name, host, username, port, password): + """Initialize the data oject.""" + self._name = name + self._host = host + self._port = port + self._username = username + self._password = password + self._status_data = None + self._sensor_data = None + self._sensor_updated_at = datetime.utcnow() + self.update() + + @property + def _base_url(self): + """Return the base url for endpoints.""" + return 'http://{}:{}'.format(self._host, self._port) + + def _request(self, path, resp='xml'): + """Make the actual request and return the parsed response.""" + url = '{}{}'.format(self._base_url, path) + + auth_tuple = () + + if self._username is not None and self._password is not None: + auth_tuple = (self._username, self._password) + + try: + response = requests.get(url, timeout=DEFAULT_TIMEOUT, + auth=auth_tuple) + if resp == 'xml': + root = ET.fromstring(response.text) + print('GOT XML', root) + return root + elif resp == 'json': + return response.json() + except requests.exceptions.HTTPError: + return {'device_state': 'error'} + except requests.exceptions.RequestException: + return {'device_state': 'offline'} + + def update_status(self): + """Get updated status information from IP Webcam.""" + return self._request('/status.json?show_avail=1', resp='json') + + def update_sensors(self): + """Get updated sensor information from IP Webcam.""" + unix_time = dt_util.as_timestamp(self._sensor_updated_at) + url = '/sensors.json?from={}'.format(unix_time) + response = self._request(url, resp='json') + self._sensor_updated_at = datetime.utcnow() + return response + + def update(self): + """Fetch the latest data from IP Webcam.""" + self._status_data = self.update_status() + self._sensor_data = self.update_sensors() + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + state_attr = {} + state_attr[ATTR_VID_CONNS] = self._status_data.get('video_connections') + state_attr[ATTR_AUD_CONNS] = self._status_data.get('audio_connections') + for (key, val) in self._status_data.get('curvals'): + if val == 'on' or val == 'off': + val = (val == 'on') + + try: + val = float(val) + except ValueError: + val = val + + state_attr[STATUS_KEY_MAP[key]] = val + return state_attr + + def change_setting(self, key, val): + """Change a setting.""" + if isinstance(val, bool): + payload = 'on' if val else 'off' + else: + payload = val + return self._request('/settings/{}?set={}'.format(key, payload)) + + def torch(self, activate=True): + """Enable/disable the torch.""" + path = '/enabletorch' if activate else '/disabletorch' + return self._request(path) + + def focus(self, activate=True): + """Enable/disable camera focus.""" + path = '/focus' if activate else '/nofocus' + return self._request(path) + + def set_front_facing_camera(self, activate=True): + """Enable/disable the front-facing camera.""" + return self.change_setting('ffc', activate) + + def set_night_vision(self, activate=True): + """Enable/disable night vision.""" + return self.change_setting('night_vision', activate) + + def set_overlay(self, activate=True): + """Enable/disable the video overlay.""" + return self.change_setting('overlay', activate) + + def set_gps_active(self, activate=True): + """Enable/disable GPS.""" + return self.change_setting('gps_active', activate) + + def set_quality(self, quality: int=100): + """Set the video quality.""" + return self.change_setting('quality', quality) + + def set_orientation(self, orientation: str='landscape'): + """Set the video orientation.""" + if orientation not in ALLOWED_ORIENTATIONS: + _LOGGER.debug('%s is not a valid orientation', orientation) + return False + return self.change_setting('orientation', orientation) + + def set_zoom(self, zoom: int): + """Set the zoom level.""" + return self._request('/settings/ptz?zoom={}'.format(zoom)) From 35084160c91596ce3eec7bf0145a9bc8fe6efbdf Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 27 Jan 2017 16:44:05 -0800 Subject: [PATCH 02/23] Some more little fixes --- homeassistant/components/android_ip_webcam.py | 17 +- .../binary_sensor/android_ip_webcam.py | 0 .../components/camera/android_ip_webcam.py | 146 ++++++++++++++++++ .../components/sensor/android_ip_webcam.py | 0 .../components/switch/android_ip_webcam.py | 0 5 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/binary_sensor/android_ip_webcam.py create mode 100644 homeassistant/components/camera/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 index b8402735e9203..db5789a8ed2e3 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -1,5 +1,5 @@ """ -Support for IP Webcam, an Android app that turns a device into a webcam. +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/ @@ -96,7 +96,7 @@ def setup(hass, config): """Setup the IP Webcam component.""" conf = config[DOMAIN] host = conf[CONF_HOST] - hass.data[DATA_IP_WEBCAM][host] = {'status': {}, 'sensors': {}} + hass.data[DATA_IP_WEBCAM][host] = IPWebcam(conf) binary_sensor_config = conf.get(CONF_BINARY_SENSORS, {}) discovery.load_platform(hass, 'binary_sensor', DOMAIN, @@ -116,13 +116,14 @@ def setup(hass, config): class IPWebcam(Entity): """The Android device running IP Webcam.""" - def __init__(self, name, host, username, port, password): + def __init__(self, config): """Initialize the data oject.""" - self._name = name - self._host = host - self._port = port - self._username = username - self._password = password + self._config = config + self._name = self._config.get(CONF_NAME) + self._host = self._config.get(CONF_HOST) + self._port = self._config.get(CONF_PORT) + self._username = self._config.get(CONF_USERNAME) + self._password = self._config.get(CONF_PASSWORD) self._status_data = None self._sensor_data = None self._sensor_updated_at = datetime.utcnow() 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 0000000000000..e69de29bb2d1d diff --git a/homeassistant/components/camera/android_ip_webcam.py b/homeassistant/components/camera/android_ip_webcam.py new file mode 100644 index 0000000000000..8d52785557b38 --- /dev/null +++ b/homeassistant/components/camera/android_ip_webcam.py @@ -0,0 +1,146 @@ +""" +Support for IP Cameras. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.mjpeg/ +""" +import asyncio +import logging +from contextlib import closing + +import aiohttp +import async_timeout +import requests +from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION, + HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) +from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) +from homeassistant.helpers.aiohttp_client import ( + async_get_clientsession, async_aiohttp_proxy_stream) +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_MJPEG_URL = 'mjpeg_url' +CONF_STILL_IMAGE_URL = 'still_image_url' +CONTENT_TYPE_HEADER = 'Content-Type' + +DEFAULT_NAME = 'Mjpeg Camera' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MJPEG_URL): cv.url, + vol.Optional(CONF_STILL_IMAGE_URL): cv.url, + vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): + vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, +}) + + +@asyncio.coroutine +# pylint: disable=unused-argument +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup a MJPEG IP Camera.""" + yield from async_add_devices([MjpegCamera(hass, config)]) + + +def extract_image_from_mjpeg(stream): + """Take in a MJPEG stream object, return the jpg from it.""" + data = b'' + for chunk in stream: + data += chunk + jpg_start = data.find(b'\xff\xd8') + jpg_end = data.find(b'\xff\xd9') + if jpg_start != -1 and jpg_end != -1: + jpg = data[jpg_start:jpg_end + 2] + return jpg + + +class MjpegCamera(Camera): + """An implementation of an IP camera that is reachable over a URL.""" + + def __init__(self, hass, device_info): + """Initialize a MJPEG camera.""" + super().__init__() + self._name = device_info.get(CONF_NAME) + self._authentication = device_info.get(CONF_AUTHENTICATION) + self._username = device_info.get(CONF_USERNAME) + self._password = device_info.get(CONF_PASSWORD) + self._mjpeg_url = device_info[CONF_MJPEG_URL] + self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL) + + self._auth = None + if self._username and self._password: + if self._authentication == HTTP_BASIC_AUTHENTICATION: + self._auth = aiohttp.BasicAuth( + self._username, password=self._password + ) + + @asyncio.coroutine + def async_camera_image(self): + """Return a still image response from the camera.""" + # DigestAuth is not supported + if self._authentication == HTTP_DIGEST_AUTHENTICATION or \ + self._still_image_url is None: + image = yield from self.hass.loop.run_in_executor( + None, self.camera_image) + return image + + websession = async_get_clientsession(self.hass) + response = None + try: + with async_timeout.timeout(10, loop=self.hass.loop): + response = yield from websession.get( + self._still_image_url, auth=self._auth) + + image = yield from response.read() + return image + + except asyncio.TimeoutError: + _LOGGER.error('Timeout getting camera image') + + except (aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError) as err: + _LOGGER.error('Error getting new camera image: %s', err) + + finally: + if response is not None: + yield from response.release() + + def camera_image(self): + """Return a still image response from the camera.""" + if self._username and self._password: + if self._authentication == HTTP_DIGEST_AUTHENTICATION: + auth = HTTPDigestAuth(self._username, self._password) + else: + auth = HTTPBasicAuth(self._username, self._password) + req = requests.get( + self._mjpeg_url, auth=auth, stream=True, timeout=10) + else: + req = requests.get(self._mjpeg_url, stream=True, timeout=10) + + with closing(req) as response: + return extract_image_from_mjpeg(response.iter_content(102400)) + + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): + """Generate an HTTP MJPEG stream from the camera.""" + # aiohttp don't support DigestAuth -> Fallback + if self._authentication == HTTP_DIGEST_AUTHENTICATION: + yield from super().handle_async_mjpeg_stream(request) + return + + # connect to stream + websession = async_get_clientsession(self.hass) + stream_coro = websession.get(self._mjpeg_url, auth=self._auth) + + yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro) + + @property + def name(self): + """Return the name of this camera.""" + return self._name diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/sensor/android_ip_webcam.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/homeassistant/components/switch/android_ip_webcam.py b/homeassistant/components/switch/android_ip_webcam.py new file mode 100644 index 0000000000000..e69de29bb2d1d From 619e3e766eed64cef4158bb3481aaed35a4b5026 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 27 Jan 2017 19:25:21 -0800 Subject: [PATCH 03/23] Checkpoint --- homeassistant/components/android_ip_webcam.py | 37 +++++--- .../binary_sensor/android_ip_webcam.py | 64 ++++++++++++++ .../components/camera/android_ip_webcam.py | 87 ++++++++----------- 3 files changed, 123 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index db5789a8ed2e3..ec1f8a23640e2 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -49,7 +49,8 @@ 'focusmode': 'Focus Mode', 'gps_active': 'GPS Active', 'idle': 'Idle', - 'ip_address': 'IP Address', + 'ip_address': 'IPv4 Address', + 'ipv6_address': 'IPv6 Address', 'ivideon_streaming': 'Ivideon Streaming', 'mirror_flip': 'Mirror Flip', 'motion_detect': 'Motion Detection', @@ -96,24 +97,27 @@ def setup(hass, config): """Setup the IP Webcam component.""" conf = config[DOMAIN] host = conf[CONF_HOST] + ip_webcam = hass.data.get(DATA_IP_WEBCAM) + if ip_webcam is None: + hass.data[DATA_IP_WEBCAM] = {} hass.data[DATA_IP_WEBCAM][host] = IPWebcam(conf) - binary_sensor_config = conf.get(CONF_BINARY_SENSORS, {}) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, - binary_sensor_config, config) + # binary_sensor_config = conf.get(CONF_BINARY_SENSORS, {}) + # discovery.load_platform(hass, 'binary_sensor', DOMAIN, + # binary_sensor_config, config) discovery.load_platform(hass, 'camera', DOMAIN, {}, config) - sensor_config = conf.get(CONF_SENSORS, {}) - discovery.load_platform(hass, 'sensor', DOMAIN, sensor_config, config) + # sensor_config = conf.get(CONF_SENSORS, {}) + # discovery.load_platform(hass, 'sensor', DOMAIN, sensor_config, config) - switch_config = conf.get(CONF_SWITCHES, {}) - discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) + # switch_config = conf.get(CONF_SWITCHES, {}) + # discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) return True -class IPWebcam(Entity): +class IPWebcam(object): """The Android device running IP Webcam.""" def __init__(self, config): @@ -185,18 +189,23 @@ def device_state_attributes(self): state_attr = {} state_attr[ATTR_VID_CONNS] = self._status_data.get('video_connections') state_attr[ATTR_AUD_CONNS] = self._status_data.get('audio_connections') - for (key, val) in self._status_data.get('curvals'): - if val == 'on' or val == 'off': - val = (val == 'on') - + for (key, val) in self._status_data.get('curvals').items(): try: val = float(val) except ValueError: val = val - state_attr[STATUS_KEY_MAP[key]] = val + if val == 'on' or val == 'off': + val = (val == 'on') + + state_attr[STATUS_KEY_MAP.get(key, key)] = val return state_attr + @property + def enabled_sensors(self): + """Return the enabled sensors.""" + return list(self._sensor_data.keys()) + def change_setting(self, key, val): """Change a setting.""" if isinstance(val, bool): diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py index e69de29bb2d1d..d8c514a86cfc8 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -0,0 +1,64 @@ +""" +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/ +""" +from itertools import chain +import logging + +from homeassistant.components.binary_sensor import (BinarySensorDevice) +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.components.android_ip_webcam import DATA_IP_WEBCAM + +DEPENDENCIES = ['android_ip_webcam'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup IP Webcam binary sensors.""" + if discovery_info is None: + return + + ip_webcam = hass.data[DATA_IP_WEBCAM] + + sensors = [] + for host, device in ip_webcam.items(): + conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, + device.enabled_sensors) + for sensor in conditions: + sensors.append(IPWebcamBinarySensor(device, sensor)) + + add_devices(sensors, True) + + +class IPWebcamBinarySensor(BinarySensorDevice): + """Represents an IP Webcam binary sensor.""" + + def __init__(self, device, variable): + """Initialize the sensor.""" + self._device = device + self.variable = variable + self._name = '{} {}'.format(self._device.name, + self.variable.replace('_', ' ')) + self._state = None + + @property + def name(self): + """Return the name of the binary 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 is_on(self): + """True if the binary sensor is on.""" + return self._state + + def update(self): + """Retrieve latest state.""" + self._state = bool(getattr(self.device._status_data, self.variable)) diff --git a/homeassistant/components/camera/android_ip_webcam.py b/homeassistant/components/camera/android_ip_webcam.py index 8d52785557b38..c7e8640408ca1 100644 --- a/homeassistant/components/camera/android_ip_webcam.py +++ b/homeassistant/components/camera/android_ip_webcam.py @@ -1,8 +1,8 @@ """ -Support for IP Cameras. +Support for IP Webcam, an Android app that acts as a full-featured webcam. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.mjpeg/ +https://home-assistant.io/components/camera.android_ip_webcam/ """ import asyncio import logging @@ -11,41 +11,31 @@ import aiohttp import async_timeout import requests -from requests.auth import HTTPBasicAuth, HTTPDigestAuth +from requests.auth import HTTPBasicAuth import voluptuous as vol -from homeassistant.const import ( - CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION, - HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.helpers.aiohttp_client import ( async_get_clientsession, async_aiohttp_proxy_stream) from homeassistant.helpers import config_validation as cv +from homeassistant.components import android_ip_webcam _LOGGER = logging.getLogger(__name__) -CONF_MJPEG_URL = 'mjpeg_url' -CONF_STILL_IMAGE_URL = 'still_image_url' -CONTENT_TYPE_HEADER = 'Content-Type' - -DEFAULT_NAME = 'Mjpeg Camera' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MJPEG_URL): cv.url, - vol.Optional(CONF_STILL_IMAGE_URL): cv.url, - vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): - vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_USERNAME): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}) +DEPENDENCIES = ['android_ip_webcam'] @asyncio.coroutine # pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Setup a MJPEG IP Camera.""" - yield from async_add_devices([MjpegCamera(hass, config)]) + """Setup an IP Webcam Camera.""" + if discovery_info is None: + return + devices = hass.data[android_ip_webcam.DATA_IP_WEBCAM] + cameras = [IPWebcamCamera(hass, device) + for key, device in devices.items()] + yield from async_add_devices(cameras, True) def extract_image_from_mjpeg(stream): @@ -60,35 +50,28 @@ def extract_image_from_mjpeg(stream): return jpg -class MjpegCamera(Camera): +class IPWebcamCamera(Camera): """An implementation of an IP camera that is reachable over a URL.""" - def __init__(self, hass, device_info): - """Initialize a MJPEG camera.""" - super().__init__() - self._name = device_info.get(CONF_NAME) - self._authentication = device_info.get(CONF_AUTHENTICATION) - self._username = device_info.get(CONF_USERNAME) - self._password = device_info.get(CONF_PASSWORD) - self._mjpeg_url = device_info[CONF_MJPEG_URL] - self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL) + def __init__(self, hass, device): + """Initialize a IP Webcam camera.""" + super(IPWebcamCamera, self).__init__() + self._device = device + self._name = self._device.name + self._username = self._device._username + self._password = self._device._password + self._mjpeg_url = '{}/{}'.format(self._device._base_url, 'video') + self._still_image_url = '{}/{}'.format(self._device._base_url, + 'photo.jpg') self._auth = None if self._username and self._password: - if self._authentication == HTTP_BASIC_AUTHENTICATION: - self._auth = aiohttp.BasicAuth( - self._username, password=self._password - ) + self._auth = aiohttp.BasicAuth(self._username, + password=self._password) @asyncio.coroutine def async_camera_image(self): """Return a still image response from the camera.""" - # DigestAuth is not supported - if self._authentication == HTTP_DIGEST_AUTHENTICATION or \ - self._still_image_url is None: - image = yield from self.hass.loop.run_in_executor( - None, self.camera_image) - return image websession = async_get_clientsession(self.hass) response = None @@ -114,10 +97,7 @@ def async_camera_image(self): def camera_image(self): """Return a still image response from the camera.""" if self._username and self._password: - if self._authentication == HTTP_DIGEST_AUTHENTICATION: - auth = HTTPDigestAuth(self._username, self._password) - else: - auth = HTTPBasicAuth(self._username, self._password) + auth = HTTPBasicAuth(self._username, self._password) req = requests.get( self._mjpeg_url, auth=auth, stream=True, timeout=10) else: @@ -129,10 +109,6 @@ def camera_image(self): @asyncio.coroutine def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - # aiohttp don't support DigestAuth -> Fallback - if self._authentication == HTTP_DIGEST_AUTHENTICATION: - yield from super().handle_async_mjpeg_stream(request) - return # connect to stream websession = async_get_clientsession(self.hass) @@ -144,3 +120,12 @@ def handle_async_mjpeg_stream(self, request): def name(self): """Return the name of this camera.""" return self._name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._device.device_state_attributes + + def update(self): + """Update the state.""" + self._device.update() From dd88c3c9a1854bc4ac45cab7a696d14b6dba7bd8 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 12:39:36 -0800 Subject: [PATCH 04/23] Checkpoint --- .../components/binary_sensor/android_ip_webcam.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py index d8c514a86cfc8..3cb50c22c4467 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -9,7 +9,8 @@ from homeassistant.components.binary_sensor import (BinarySensorDevice) from homeassistant.const import CONF_MONITORED_CONDITIONS -from homeassistant.components.android_ip_webcam import DATA_IP_WEBCAM +from homeassistant.components.android_ip_webcam import (STATUS_KEY_MAP, + DATA_IP_WEBCAM) DEPENDENCIES = ['android_ip_webcam'] @@ -49,11 +50,6 @@ def name(self): """Return the name of the binary 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 is_on(self): """True if the binary sensor is on.""" @@ -61,4 +57,5 @@ def is_on(self): def update(self): """Retrieve latest state.""" - self._state = bool(getattr(self.device._status_data, self.variable)) + mapped_key = STATUS_KEY_MAP.get(self.variable) + self._state = self.device.device_state_attributes.get(mapped_key) From e618754ad45b95f255b9335a0f2cdbbbf5e3687f Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 14:04:23 -0800 Subject: [PATCH 05/23] Binary sensor working --- homeassistant/components/android_ip_webcam.py | 78 ++++++++++++------- .../binary_sensor/android_ip_webcam.py | 26 +++---- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index ec1f8a23640e2..e2e849dd2e8e0 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -5,7 +5,7 @@ https://home-assistant.io/components/android_ip_webcam/ """ import logging -from datetime import datetime +from datetime import datetime, timedelta import xml.etree.ElementTree as ET import requests @@ -13,12 +13,10 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT, - CONF_USERNAME, CONF_PASSWORD, - CONF_BINARY_SENSORS, CONF_SENSORS, - CONF_SWITCHES, CONF_MONITORED_CONDITIONS) + CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, + CONF_SWITCHES) _LOGGER = logging.getLogger(__name__) @@ -72,9 +70,33 @@ 'zoom': 'Zoom' } -SENSOR_SCHEMA = vol.Schema({ - vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list) -}) +SENSOR_KEY_MAP = { + 'battery_level': 'Battery Level', + 'battery_temp': 'Battery Temperature', + 'battery_voltage': 'Battery Voltage', + 'light': 'Light Level', + 'motion': 'Motion', + 'motion_event': 'Motion Event', + 'motion_active': 'Motion Active', + 'pressure': 'Pressure', + 'proximity': 'Proximity', + 'sound': 'Sound', + 'sound_event': 'Sound Event', + 'sound_timeout': 'Sound Timeout' +} + +SWITCHES = ['audio_only', 'exposure_lock', 'ffc', + 'focus', 'focus_homing', 'gps_active', 'idle', + 'ivideon_streaming', 'motion_detect', 'night_vision', + 'overlay', 'torch', 'video_recording', 'whitebalance_lock'] + +SENSORS = ['battery_level', 'battery_temp', 'battery_voltage', + 'light', 'motion', 'motion_event', 'pressure', 'proximity', + 'sound', 'sound_event', 'sound_timeout'] + +CONF_MOTION_BINARY_SENSOR = 'motion_binary_sensor' + +DEFAULT_MOTION_BINARY_SENSOR = True CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -83,9 +105,14 @@ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, - vol.Optional(CONF_SWITCHES): SENSOR_SCHEMA, - vol.Optional(CONF_SENSORS): SENSOR_SCHEMA, - vol.Optional(CONF_BINARY_SENSORS): SENSOR_SCHEMA + 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_BINARY_SENSOR, + default=DEFAULT_MOTION_BINARY_SENSOR): bool }) }, extra=vol.ALLOW_EXTRA) @@ -102,9 +129,8 @@ def setup(hass, config): hass.data[DATA_IP_WEBCAM] = {} hass.data[DATA_IP_WEBCAM][host] = IPWebcam(conf) - # binary_sensor_config = conf.get(CONF_BINARY_SENSORS, {}) - # discovery.load_platform(hass, 'binary_sensor', DOMAIN, - # binary_sensor_config, config) + if conf.get(CONF_MOTION_BINARY_SENSOR, False) is True: + discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) discovery.load_platform(hass, 'camera', DOMAIN, {}, config) @@ -128,9 +154,10 @@ def __init__(self, config): self._port = self._config.get(CONF_PORT) self._username = self._config.get(CONF_USERNAME) self._password = self._config.get(CONF_PASSWORD) - self._status_data = None - self._sensor_data = None - self._sensor_updated_at = datetime.utcnow() + self.status_data = None + self.sensor_data = None + # Let's get the data for the last 15 seconds since it's the first start + self._sensor_updated_at = (datetime.now() - timedelta(seconds=15)) self.update() @property @@ -152,7 +179,6 @@ def _request(self, path, resp='xml'): auth=auth_tuple) if resp == 'xml': root = ET.fromstring(response.text) - print('GOT XML', root) return root elif resp == 'json': return response.json() @@ -167,16 +193,16 @@ def update_status(self): def update_sensors(self): """Get updated sensor information from IP Webcam.""" - unix_time = dt_util.as_timestamp(self._sensor_updated_at) + unix_time = int((dt_util.as_timestamp(self._sensor_updated_at)*1000)) url = '/sensors.json?from={}'.format(unix_time) response = self._request(url, resp='json') - self._sensor_updated_at = datetime.utcnow() + self._sensor_updated_at = datetime.now() return response def update(self): """Fetch the latest data from IP Webcam.""" - self._status_data = self.update_status() - self._sensor_data = self.update_sensors() + self.status_data = self.update_status() + self.sensor_data = self.update_sensors() @property def name(self): @@ -187,9 +213,9 @@ def name(self): def device_state_attributes(self): """Return the state attributes.""" state_attr = {} - state_attr[ATTR_VID_CONNS] = self._status_data.get('video_connections') - state_attr[ATTR_AUD_CONNS] = self._status_data.get('audio_connections') - for (key, val) in self._status_data.get('curvals').items(): + state_attr[ATTR_VID_CONNS] = self.status_data.get('video_connections') + state_attr[ATTR_AUD_CONNS] = self.status_data.get('audio_connections') + for (key, val) in self.status_data.get('curvals').items(): try: val = float(val) except ValueError: @@ -204,7 +230,7 @@ def device_state_attributes(self): @property def enabled_sensors(self): """Return the enabled sensors.""" - return list(self._sensor_data.keys()) + return list(self.sensor_data.keys()) def change_setting(self, key, val): """Change a setting.""" diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py index 3cb50c22c4467..9cb779a2a5374 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -4,12 +4,10 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.android_ip_webcam/ """ -from itertools import chain import logging from homeassistant.components.binary_sensor import (BinarySensorDevice) -from homeassistant.const import CONF_MONITORED_CONDITIONS -from homeassistant.components.android_ip_webcam import (STATUS_KEY_MAP, +from homeassistant.components.android_ip_webcam import (SENSOR_KEY_MAP, DATA_IP_WEBCAM) DEPENDENCIES = ['android_ip_webcam'] @@ -24,14 +22,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ip_webcam = hass.data[DATA_IP_WEBCAM] - sensors = [] - for host, device in ip_webcam.items(): - conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, - device.enabled_sensors) - for sensor in conditions: - sensors.append(IPWebcamBinarySensor(device, sensor)) - - add_devices(sensors, True) + for device in ip_webcam.values(): + add_devices([IPWebcamBinarySensor(device, 'motion_active')], True) class IPWebcamBinarySensor(BinarySensorDevice): @@ -41,9 +33,8 @@ def __init__(self, device, variable): """Initialize the sensor.""" self._device = device self.variable = variable - self._name = '{} {}'.format(self._device.name, - self.variable.replace('_', ' ')) - self._state = None + self._mapped_name = SENSOR_KEY_MAP.get(self.variable, self.variable) + self._name = '{} {}'.format(self._device.name, self._mapped_name) @property def name(self): @@ -53,9 +44,10 @@ def name(self): @property def is_on(self): """True if the binary sensor is on.""" - return self._state + container = self._device.sensor_data.get(self.variable) + data_point = container.get('data', [[0, [0.0]]]) + return data_point[0][-1][0] == 1.0 def update(self): """Retrieve latest state.""" - mapped_key = STATUS_KEY_MAP.get(self.variable) - self._state = self.device.device_state_attributes.get(mapped_key) + self._device.update() From 5c7d6194027b8acba1b75f78d47b5a9eec91888b Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 14:06:50 -0800 Subject: [PATCH 06/23] Flake8 and PyLint fixes --- homeassistant/components/android_ip_webcam.py | 18 +++++++++--------- .../components/camera/android_ip_webcam.py | 13 +++++-------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index e2e849dd2e8e0..e69693caf2d0d 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -149,11 +149,11 @@ class IPWebcam(object): def __init__(self, config): """Initialize the data oject.""" self._config = config - self._name = self._config.get(CONF_NAME) - self._host = self._config.get(CONF_HOST) - self._port = self._config.get(CONF_PORT) - self._username = self._config.get(CONF_USERNAME) - self._password = self._config.get(CONF_PASSWORD) + self.name = self._config.get(CONF_NAME) + self.host = self._config.get(CONF_HOST) + self.port = self._config.get(CONF_PORT) + self.username = self._config.get(CONF_USERNAME) + self.password = self._config.get(CONF_PASSWORD) self.status_data = None self.sensor_data = None # Let's get the data for the last 15 seconds since it's the first start @@ -163,7 +163,7 @@ def __init__(self, config): @property def _base_url(self): """Return the base url for endpoints.""" - return 'http://{}:{}'.format(self._host, self._port) + return 'http://{}:{}'.format(self.host, self.port) def _request(self, path, resp='xml'): """Make the actual request and return the parsed response.""" @@ -171,8 +171,8 @@ def _request(self, path, resp='xml'): auth_tuple = () - if self._username is not None and self._password is not None: - auth_tuple = (self._username, self._password) + if self.username is not None and self.password is not None: + auth_tuple = (self.username, self.password) try: response = requests.get(url, timeout=DEFAULT_TIMEOUT, @@ -207,7 +207,7 @@ def update(self): @property def name(self): """Return the name of the device.""" - return self._name + return self.name @property def device_state_attributes(self): diff --git a/homeassistant/components/camera/android_ip_webcam.py b/homeassistant/components/camera/android_ip_webcam.py index c7e8640408ca1..87448d6338fec 100644 --- a/homeassistant/components/camera/android_ip_webcam.py +++ b/homeassistant/components/camera/android_ip_webcam.py @@ -12,12 +12,10 @@ import async_timeout import requests from requests.auth import HTTPBasicAuth -import voluptuous as vol from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.helpers.aiohttp_client import ( async_get_clientsession, async_aiohttp_proxy_stream) -from homeassistant.helpers import config_validation as cv from homeassistant.components import android_ip_webcam _LOGGER = logging.getLogger(__name__) @@ -26,6 +24,7 @@ DEPENDENCIES = ['android_ip_webcam'] + @asyncio.coroutine # pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): @@ -58,10 +57,10 @@ def __init__(self, hass, device): super(IPWebcamCamera, self).__init__() self._device = device self._name = self._device.name - self._username = self._device._username - self._password = self._device._password - self._mjpeg_url = '{}/{}'.format(self._device._base_url, 'video') - self._still_image_url = '{}/{}'.format(self._device._base_url, + self._username = self._device.username + self._password = self._device.password + self._mjpeg_url = '{}/{}'.format(self._device.base_url, 'video') + self._still_image_url = '{}/{}'.format(self._device.base_url, 'photo.jpg') self._auth = None @@ -72,7 +71,6 @@ def __init__(self, hass, device): @asyncio.coroutine def async_camera_image(self): """Return a still image response from the camera.""" - websession = async_get_clientsession(self.hass) response = None try: @@ -109,7 +107,6 @@ def camera_image(self): @asyncio.coroutine def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - # connect to stream websession = async_get_clientsession(self.hass) stream_coro = websession.get(self._mjpeg_url, auth=self._auth) From 848bd43a0c8c5ecf21d14c1795e3d948340ba057 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 14:31:45 -0800 Subject: [PATCH 07/23] Remove unneeded parentheses --- homeassistant/components/binary_sensor/android_ip_webcam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py index 9cb779a2a5374..ff73dafc30d73 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -6,7 +6,7 @@ """ import logging -from homeassistant.components.binary_sensor import (BinarySensorDevice) +from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.android_ip_webcam import (SENSOR_KEY_MAP, DATA_IP_WEBCAM) From b325e29241d21696ba38d2677ce2ab61e58a22ac Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 14:32:02 -0800 Subject: [PATCH 08/23] Complete the sensor platform --- homeassistant/components/android_ip_webcam.py | 31 +++----- .../components/sensor/android_ip_webcam.py | 77 +++++++++++++++++++ 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index e69693caf2d0d..7d127a3d6eb71 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -5,7 +5,6 @@ https://home-assistant.io/components/android_ip_webcam/ """ import logging -from datetime import datetime, timedelta import xml.etree.ElementTree as ET import requests @@ -13,7 +12,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery -import homeassistant.util.dt as dt_util from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, CONF_SWITCHES) @@ -91,8 +89,8 @@ 'overlay', 'torch', 'video_recording', 'whitebalance_lock'] SENSORS = ['battery_level', 'battery_temp', 'battery_voltage', - 'light', 'motion', 'motion_event', 'pressure', 'proximity', - 'sound', 'sound_event', 'sound_timeout'] + 'light', 'motion', 'pressure', 'proximity', 'sound', 'sound_event', + 'sound_timeout'] CONF_MOTION_BINARY_SENSOR = 'motion_binary_sensor' @@ -134,10 +132,10 @@ def setup(hass, config): discovery.load_platform(hass, 'camera', DOMAIN, {}, config) - # sensor_config = conf.get(CONF_SENSORS, {}) - # discovery.load_platform(hass, 'sensor', DOMAIN, sensor_config, config) + sensor_config = conf.get(CONF_SENSORS, []) + discovery.load_platform(hass, 'sensor', DOMAIN, sensor_config, config) - # switch_config = conf.get(CONF_SWITCHES, {}) + # switch_config = conf.get(CONF_SWITCHES, []) # discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) return True @@ -149,25 +147,23 @@ class IPWebcam(object): def __init__(self, config): """Initialize the data oject.""" self._config = config - self.name = self._config.get(CONF_NAME) + self._name = self._config.get(CONF_NAME) self.host = self._config.get(CONF_HOST) self.port = self._config.get(CONF_PORT) self.username = self._config.get(CONF_USERNAME) self.password = self._config.get(CONF_PASSWORD) self.status_data = None self.sensor_data = None - # Let's get the data for the last 15 seconds since it's the first start - self._sensor_updated_at = (datetime.now() - timedelta(seconds=15)) self.update() @property - def _base_url(self): + def base_url(self): """Return the base url for endpoints.""" return 'http://{}:{}'.format(self.host, self.port) def _request(self, path, resp='xml'): """Make the actual request and return the parsed response.""" - url = '{}{}'.format(self._base_url, path) + url = '{}{}'.format(self.base_url, path) auth_tuple = () @@ -178,8 +174,7 @@ def _request(self, path, resp='xml'): response = requests.get(url, timeout=DEFAULT_TIMEOUT, auth=auth_tuple) if resp == 'xml': - root = ET.fromstring(response.text) - return root + return ET.fromstring(response.text) elif resp == 'json': return response.json() except requests.exceptions.HTTPError: @@ -193,11 +188,7 @@ def update_status(self): def update_sensors(self): """Get updated sensor information from IP Webcam.""" - unix_time = int((dt_util.as_timestamp(self._sensor_updated_at)*1000)) - url = '/sensors.json?from={}'.format(unix_time) - response = self._request(url, resp='json') - self._sensor_updated_at = datetime.now() - return response + return self._request('/sensors.json', resp='json') def update(self): """Fetch the latest data from IP Webcam.""" @@ -207,7 +198,7 @@ def update(self): @property def name(self): """Return the name of the device.""" - return self.name + return self._name @property def device_state_attributes(self): diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/sensor/android_ip_webcam.py index e69de29bb2d1d..44b3d8bc3c967 100644 --- a/homeassistant/components/sensor/android_ip_webcam.py +++ b/homeassistant/components/sensor/android_ip_webcam.py @@ -0,0 +1,77 @@ +""" +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 logging + +from homeassistant.components.android_ip_webcam import (SENSOR_KEY_MAP, + DATA_IP_WEBCAM) +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = ['android_ip_webcam'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the IP Webcam Sensor.""" + if discovery_info is None: + return + + ip_webcam = hass.data[DATA_IP_WEBCAM] + + all_sensors = [] + + for device in ip_webcam.values(): + for sensor in discovery_info: + all_sensors.append(IPWebcamSensor(device, sensor)) + + add_devices(all_sensors, True) + + return True + + +class IPWebcamSensor(Entity): + """Representation of a IP Webcam sensor.""" + + def __init__(self, device, variable): + """Initialize the sensor.""" + self._device = device + self.variable = variable + + # device specific + self._mapped_name = SENSOR_KEY_MAP.get(self.variable, self.variable) + self._name = '{} {}'.format(self._device.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 + + def update(self): + """Retrieve latest state.""" + self._device.update() + container = self._device.sensor_data.get(self.variable) + 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 device_state_attributes(self): + """Return the state attributes.""" + return self._device.device_state_attributes From 84b3c1a05b488a221d9189ca7a8f414b298d1b80 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 15:11:19 -0800 Subject: [PATCH 09/23] Add switch platform --- homeassistant/components/android_ip_webcam.py | 68 +++++++++------ .../binary_sensor/android_ip_webcam.py | 4 +- .../components/sensor/android_ip_webcam.py | 17 ++-- .../components/switch/android_ip_webcam.py | 86 +++++++++++++++++++ 4 files changed, 140 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 7d127a3d6eb71..a2532a9419c93 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -30,10 +30,14 @@ ATTR_VID_CONNS = 'Video Connections' ATTR_AUD_CONNS = 'Audio Connections' -STATUS_KEY_MAP = { +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', @@ -48,8 +52,12 @@ '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', @@ -57,9 +65,15 @@ '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', @@ -68,29 +82,13 @@ 'zoom': 'Zoom' } -SENSOR_KEY_MAP = { - 'battery_level': 'Battery Level', - 'battery_temp': 'Battery Temperature', - 'battery_voltage': 'Battery Voltage', - 'light': 'Light Level', - 'motion': 'Motion', - 'motion_event': 'Motion Event', - 'motion_active': 'Motion Active', - 'pressure': 'Pressure', - 'proximity': 'Proximity', - 'sound': 'Sound', - 'sound_event': 'Sound Event', - 'sound_timeout': 'Sound Timeout' -} - -SWITCHES = ['audio_only', 'exposure_lock', 'ffc', - 'focus', 'focus_homing', 'gps_active', 'idle', - 'ivideon_streaming', 'motion_detect', 'night_vision', - 'overlay', 'torch', 'video_recording', 'whitebalance_lock'] +SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision', + 'overlay', 'quality', 'torch', 'video_chunk_len', + 'whitebalance_lock'] -SENSORS = ['battery_level', 'battery_temp', 'battery_voltage', - 'light', 'motion', 'pressure', 'proximity', 'sound', 'sound_event', - 'sound_timeout'] +SENSORS = ['audio_connections', 'battery_level', 'battery_temp', + 'battery_voltage', 'light', 'motion', 'pressure', 'proximity', + 'sound', 'sound_event', 'sound_timeout', 'video_connections'] CONF_MOTION_BINARY_SENSOR = 'motion_binary_sensor' @@ -135,8 +133,8 @@ def setup(hass, config): sensor_config = conf.get(CONF_SENSORS, []) discovery.load_platform(hass, 'sensor', DOMAIN, sensor_config, config) - # switch_config = conf.get(CONF_SWITCHES, []) - # discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) + switch_config = conf.get(CONF_SWITCHES, []) + discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) return True @@ -170,6 +168,8 @@ def _request(self, path, resp='xml'): if self.username is not None and self.password is not None: auth_tuple = (self.username, self.password) + print('URL', url) + try: response = requests.get(url, timeout=DEFAULT_TIMEOUT, auth=auth_tuple) @@ -215,7 +215,7 @@ def device_state_attributes(self): if val == 'on' or val == 'off': val = (val == 'on') - state_attr[STATUS_KEY_MAP.get(key, key)] = val + state_attr[KEY_MAP.get(key, key)] = val return state_attr @property @@ -223,6 +223,22 @@ def enabled_sensors(self): """Return the enabled sensors.""" return list(self.sensor_data.keys()) + @property + def current_settings(self): + """Return a dictionary of the current settings.""" + settings = {} + for (key, val) in self.status_data.get('curvals').items(): + try: + val = float(val) + except ValueError: + val = val + + if val == 'on' or val == 'off': + val = (val == 'on') + + settings[key] = val + return settings + def change_setting(self, key, val): """Change a setting.""" if isinstance(val, bool): diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py index ff73dafc30d73..b558a9180cdb5 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -7,7 +7,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.android_ip_webcam import (SENSOR_KEY_MAP, +from homeassistant.components.android_ip_webcam import (KEY_MAP, DATA_IP_WEBCAM) DEPENDENCIES = ['android_ip_webcam'] @@ -33,7 +33,7 @@ def __init__(self, device, variable): """Initialize the sensor.""" self._device = device self.variable = variable - self._mapped_name = SENSOR_KEY_MAP.get(self.variable, self.variable) + self._mapped_name = KEY_MAP.get(self.variable, self.variable) self._name = '{} {}'.format(self._device.name, self._mapped_name) @property diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/sensor/android_ip_webcam.py index 44b3d8bc3c967..91135c26af3d6 100644 --- a/homeassistant/components/sensor/android_ip_webcam.py +++ b/homeassistant/components/sensor/android_ip_webcam.py @@ -6,7 +6,7 @@ """ import logging -from homeassistant.components.android_ip_webcam import (SENSOR_KEY_MAP, +from homeassistant.components.android_ip_webcam import (KEY_MAP, DATA_IP_WEBCAM) from homeassistant.helpers.entity import Entity @@ -42,7 +42,7 @@ def __init__(self, device, variable): self.variable = variable # device specific - self._mapped_name = SENSOR_KEY_MAP.get(self.variable, self.variable) + self._mapped_name = KEY_MAP.get(self.variable, self.variable) self._name = '{} {}'.format(self._device.name, self._mapped_name) self._state = None self._unit = None @@ -65,11 +65,14 @@ def state(self): def update(self): """Retrieve latest state.""" self._device.update() - container = self._device.sensor_data.get(self.variable) - 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] + if self.variable in ('audio_connections', 'video_connections'): + self._state = self._device.status_data.get(self.variable) + else: + container = self._device.sensor_data.get(self.variable) + 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 device_state_attributes(self): diff --git a/homeassistant/components/switch/android_ip_webcam.py b/homeassistant/components/switch/android_ip_webcam.py index e69de29bb2d1d..badf27b837c1a 100644 --- a/homeassistant/components/switch/android_ip_webcam.py +++ b/homeassistant/components/switch/android_ip_webcam.py @@ -0,0 +1,86 @@ +""" +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 logging + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.android_ip_webcam import (KEY_MAP, + DATA_IP_WEBCAM) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['android_ip_webcam'] +DOMAIN = 'switch' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the IP Webcam switch platform.""" + if discovery_info is None: + return + + ip_webcam = hass.data[DATA_IP_WEBCAM] + + all_switches = [] + + for device in ip_webcam.values(): + for setting in discovery_info: + all_switches.append(IPWebcamSettingsSwitch(device, setting)) + + add_devices(all_switches, True) + + return True + + +class IPWebcamSettingsSwitch(SwitchDevice): + """An abstract class for an IP Webcam setting.""" + + def __init__(self, device, setting): + """Initialize the settings switch.""" + self._device = device + 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 + + def update(self): + """Get the updated status of the switch.""" + self._device.update() + self._state = self._device.current_settings.get(self._setting) + + @property + def is_on(self): + """Return the boolean response if the node is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn device on.""" + if self._setting is 'torch': + self._device.torch(activate=True) + elif self._setting is 'focus': + self._device.focus(activate=True) + else: + self._device.change_setting(self._setting, True) + self._state = True + + def turn_off(self, **kwargs): + """Turn device off.""" + if self._setting is 'torch': + self._device.torch(activate=False) + elif self._setting is 'focus': + self._device.focus(activate=False) + else: + self._device.change_setting(self._setting, False) + self._state = False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._device.device_state_attributes From f0b843e5fea55cb3ff8dc5752aae985431581888 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 15:34:31 -0800 Subject: [PATCH 10/23] Add icons --- homeassistant/components/android_ip_webcam.py | 25 +++++++++++++++++++ .../binary_sensor/android_ip_webcam.py | 15 ++++++++--- .../components/sensor/android_ip_webcam.py | 18 ++++++++++++- .../components/switch/android_ip_webcam.py | 7 +++++- 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index a2532a9419c93..9034fec31ff40 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -82,6 +82,31 @@ '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', + 'whitebalance_lock': 'mdi:white-balance-auto' +} + SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision', 'overlay', 'quality', 'torch', 'video_chunk_len', 'whitebalance_lock'] diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py index b558a9180cdb5..8d4f967777e48 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -7,7 +7,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.android_ip_webcam import (KEY_MAP, +from homeassistant.components.android_ip_webcam import (KEY_MAP, ICON_MAP, DATA_IP_WEBCAM) DEPENDENCIES = ['android_ip_webcam'] @@ -35,6 +35,7 @@ def __init__(self, device, variable): self.variable = variable self._mapped_name = KEY_MAP.get(self.variable, self.variable) self._name = '{} {}'.format(self._device.name, self._mapped_name) + self._state = None @property def name(self): @@ -44,10 +45,16 @@ def name(self): @property def is_on(self): """True if the binary sensor is on.""" - container = self._device.sensor_data.get(self.variable) - data_point = container.get('data', [[0, [0.0]]]) - return data_point[0][-1][0] == 1.0 + return self._state def update(self): """Retrieve latest state.""" self._device.update() + container = self._device.sensor_data.get(self.variable) + data_point = container.get('data', [[0, [0.0]]]) + self._state = data_point[0][-1][0] == 1.0 + + @property + def icon(self): + """Return the icon for the sensor.""" + return 'mdi:run' if self._state else 'mdi:walk' diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/sensor/android_ip_webcam.py index 91135c26af3d6..647920c5c75a0 100644 --- a/homeassistant/components/sensor/android_ip_webcam.py +++ b/homeassistant/components/sensor/android_ip_webcam.py @@ -6,7 +6,7 @@ """ import logging -from homeassistant.components.android_ip_webcam import (KEY_MAP, +from homeassistant.components.android_ip_webcam import (KEY_MAP, ICON_MAP, DATA_IP_WEBCAM) from homeassistant.helpers.entity import Entity @@ -78,3 +78,19 @@ def update(self): def device_state_attributes(self): """Return the state attributes.""" return self._device.device_state_attributes + + @property + def icon(self): + """Return the icon for the sensor.""" + if self.variable == 'battery_level': + 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.variable, 'mdi:eye') diff --git a/homeassistant/components/switch/android_ip_webcam.py b/homeassistant/components/switch/android_ip_webcam.py index badf27b837c1a..749aa397ad4d7 100644 --- a/homeassistant/components/switch/android_ip_webcam.py +++ b/homeassistant/components/switch/android_ip_webcam.py @@ -7,7 +7,7 @@ import logging from homeassistant.components.switch import SwitchDevice -from homeassistant.components.android_ip_webcam import (KEY_MAP, +from homeassistant.components.android_ip_webcam import (KEY_MAP, ICON_MAP, DATA_IP_WEBCAM) _LOGGER = logging.getLogger(__name__) @@ -84,3 +84,8 @@ def turn_off(self, **kwargs): def device_state_attributes(self): """Return the state attributes.""" return self._device.device_state_attributes + + @property + def icon(self): + """Return the icon for the switch.""" + return ICON_MAP.get(self._setting, 'mdi:flash') From f7aa70b7421f6dd8b130a94090dcab22a1b98fdf Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 15:46:50 -0800 Subject: [PATCH 11/23] Add throttle and re-add sensor from time --- homeassistant/components/android_ip_webcam.py | 26 +++++++++++-------- .../binary_sensor/android_ip_webcam.py | 3 ++- .../components/sensor/android_ip_webcam.py | 2 ++ .../components/switch/android_ip_webcam.py | 1 + 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 9034fec31ff40..5e58c0e101b7d 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -5,6 +5,7 @@ https://home-assistant.io/components/android_ip_webcam/ """ import logging +from datetime import datetime, timedelta import xml.etree.ElementTree as ET import requests @@ -12,9 +13,11 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery +import homeassistant.util.dt as dt_util from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, CONF_SWITCHES) +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -140,6 +143,8 @@ ALLOWED_ORIENTATIONS = ['landscape', 'upsidedown', 'portrait', 'upsidedown_portrait'] +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=3) + def setup(hass, config): """Setup the IP Webcam component.""" @@ -177,6 +182,7 @@ def __init__(self, config): self.password = self._config.get(CONF_PASSWORD) self.status_data = None self.sensor_data = None + self._sensor_updated_at = (datetime.now() - timedelta(seconds=5)) self.update() @property @@ -184,7 +190,7 @@ def base_url(self): """Return the base url for endpoints.""" return 'http://{}:{}'.format(self.host, self.port) - def _request(self, path, resp='xml'): + def _request(self, path): """Make the actual request and return the parsed response.""" url = '{}{}'.format(self.base_url, path) @@ -195,6 +201,8 @@ def _request(self, path, resp='xml'): print('URL', url) + resp = 'json' if '.json' in path else 'xml' + try: response = requests.get(url, timeout=DEFAULT_TIMEOUT, auth=auth_tuple) @@ -207,18 +215,14 @@ def _request(self, path, resp='xml'): except requests.exceptions.RequestException: return {'device_state': 'offline'} - def update_status(self): - """Get updated status information from IP Webcam.""" - return self._request('/status.json?show_avail=1', resp='json') - - def update_sensors(self): - """Get updated sensor information from IP Webcam.""" - return self._request('/sensors.json', resp='json') - + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Fetch the latest data from IP Webcam.""" - self.status_data = self.update_status() - self.sensor_data = self.update_sensors() + self.status_data = self._request('/status.json') + + utime = int(dt_util.as_timestamp(self._sensor_updated_at) * 1000) + self.sensor_data = self._request('/sensors.json?from={}'.format(utime)) + self._sensor_updated_at = datetime.now() @property def name(self): diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py index 8d4f967777e48..08779842b0034 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -7,7 +7,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.android_ip_webcam import (KEY_MAP, ICON_MAP, +from homeassistant.components.android_ip_webcam import (KEY_MAP, DATA_IP_WEBCAM) DEPENDENCIES = ['android_ip_webcam'] @@ -36,6 +36,7 @@ def __init__(self, device, variable): self._mapped_name = KEY_MAP.get(self.variable, self.variable) self._name = '{} {}'.format(self._device.name, self._mapped_name) self._state = None + self.update() @property def name(self): diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/sensor/android_ip_webcam.py index 647920c5c75a0..a021bd214f8d2 100644 --- a/homeassistant/components/sensor/android_ip_webcam.py +++ b/homeassistant/components/sensor/android_ip_webcam.py @@ -46,6 +46,7 @@ def __init__(self, device, variable): self._name = '{} {}'.format(self._device.name, self._mapped_name) self._state = None self._unit = None + self.update() @property def name(self): @@ -67,6 +68,7 @@ def update(self): self._device.update() if self.variable in ('audio_connections', 'video_connections'): self._state = self._device.status_data.get(self.variable) + self._unit = 'Connections' else: container = self._device.sensor_data.get(self.variable) self._unit = container.get('unit', self._unit) diff --git a/homeassistant/components/switch/android_ip_webcam.py b/homeassistant/components/switch/android_ip_webcam.py index 749aa397ad4d7..53e0794cd35b0 100644 --- a/homeassistant/components/switch/android_ip_webcam.py +++ b/homeassistant/components/switch/android_ip_webcam.py @@ -44,6 +44,7 @@ def __init__(self, device, setting): self._mapped_name = KEY_MAP.get(self._setting, self._setting) self._name = '{} {}'.format(self._device.name, self._mapped_name) self._state = False + self.update() @property def name(self): From f536da9aff74603cafefd01ffdd3df86e8e64f95 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 15:52:39 -0800 Subject: [PATCH 12/23] Remove sound_event and sound_timeout --- homeassistant/components/android_ip_webcam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 5e58c0e101b7d..3d8b69e41354e 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -116,7 +116,7 @@ SENSORS = ['audio_connections', 'battery_level', 'battery_temp', 'battery_voltage', 'light', 'motion', 'pressure', 'proximity', - 'sound', 'sound_event', 'sound_timeout', 'video_connections'] + 'sound', 'video_connections'] CONF_MOTION_BINARY_SENSOR = 'motion_binary_sensor' From 3df91bfc7aa9349a10b19a943fcaddda92510c22 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 15:52:52 -0800 Subject: [PATCH 13/23] Remove stale print line --- homeassistant/components/android_ip_webcam.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 3d8b69e41354e..6d74105440134 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -199,8 +199,6 @@ def _request(self, path): if self.username is not None and self.password is not None: auth_tuple = (self.username, self.password) - print('URL', url) - resp = 'json' if '.json' in path else 'xml' try: From 4fe7c594e57700272f686f16ec24877a3de07591 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 15:58:58 -0800 Subject: [PATCH 14/23] Correct the list of valid switches --- 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 6d74105440134..cd0ec9fd55246 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -110,8 +110,9 @@ 'whitebalance_lock': 'mdi:white-balance-auto' } -SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision', - 'overlay', 'quality', 'torch', 'video_chunk_len', +SWITCHES = ['audio_only', 'exposure_lock', 'ffc', 'focus', + 'focus_homing', 'gps_active', 'idle', 'ivideon_streaming', + 'motion_detect', 'night_vision', 'overlay', 'torch', 'whitebalance_lock'] SENSORS = ['audio_connections', 'battery_level', 'battery_temp', From 6059f456c6646ed42a316a0ab47e8f7ecf542374 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 16:14:34 -0800 Subject: [PATCH 15/23] The actually right list of switches --- homeassistant/components/android_ip_webcam.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index cd0ec9fd55246..52288a8a22c67 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -110,10 +110,8 @@ 'whitebalance_lock': 'mdi:white-balance-auto' } -SWITCHES = ['audio_only', 'exposure_lock', 'ffc', 'focus', - 'focus_homing', 'gps_active', 'idle', 'ivideon_streaming', - 'motion_detect', 'night_vision', 'overlay', 'torch', - 'whitebalance_lock'] +SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision', + 'overlay', 'torch', 'whitebalance_lock'] SENSORS = ['audio_connections', 'battery_level', 'battery_temp', 'battery_voltage', 'light', 'motion', 'pressure', 'proximity', From 1979b71ad9bcddb8b97093d590f34104de4fa719 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 16:23:29 -0800 Subject: [PATCH 16/23] Add recording support --- homeassistant/components/android_ip_webcam.py | 14 +++++++++++++- .../components/switch/android_ip_webcam.py | 4 ++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 52288a8a22c67..6f632ec847844 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -8,6 +8,7 @@ from datetime import datetime, timedelta import xml.etree.ElementTree as ET import requests +from urllib.parse import quote import voluptuous as vol @@ -107,11 +108,12 @@ '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'] + 'overlay', 'torch', 'whitebalance_lock', 'video_recording'] SENSORS = ['audio_connections', 'battery_level', 'battery_temp', 'battery_voltage', 'light', 'motion', 'pressure', 'proximity', @@ -200,6 +202,9 @@ def _request(self, path): resp = 'json' if '.json' in path else 'xml' + if '/startvideo' in path or '/stopvideo' in path: + resp = 'json' + try: response = requests.get(url, timeout=DEFAULT_TIMEOUT, auth=auth_tuple) @@ -283,6 +288,13 @@ def focus(self, activate=True): path = '/focus' if activate else '/nofocus' return self._request(path) + def record(self, record=True, tag=None): + """Enable/disable recording.""" + path = '/startvideo?force=1' if record else '/stopvideo?force=1' + if record and tag is not None: + path = '/startvideo?force=1&tag={}'.format(quote(tag)) + return self._request(path) + def set_front_facing_camera(self, activate=True): """Enable/disable the front-facing camera.""" return self.change_setting('ffc', activate) diff --git a/homeassistant/components/switch/android_ip_webcam.py b/homeassistant/components/switch/android_ip_webcam.py index 53e0794cd35b0..142f1cd92f233 100644 --- a/homeassistant/components/switch/android_ip_webcam.py +++ b/homeassistant/components/switch/android_ip_webcam.py @@ -67,6 +67,8 @@ def turn_on(self, **kwargs): self._device.torch(activate=True) elif self._setting is 'focus': self._device.focus(activate=True) + elif self._setting is 'video_recording': + self._device.record(record=True) else: self._device.change_setting(self._setting, True) self._state = True @@ -77,6 +79,8 @@ def turn_off(self, **kwargs): self._device.torch(activate=False) elif self._setting is 'focus': self._device.focus(activate=False) + elif self._setting is 'video_recording': + self._device.record(record=False) else: self._device.change_setting(self._setting, False) self._state = False From b7b7ec29f462ba8880c12f0377ca8dea93df748c Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 16:36:44 -0800 Subject: [PATCH 17/23] Fix wrong import order --- homeassistant/components/android_ip_webcam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 6f632ec847844..dec545ca7ceb7 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -7,8 +7,8 @@ import logging from datetime import datetime, timedelta import xml.etree.ElementTree as ET -import requests from urllib.parse import quote +import requests import voluptuous as vol From f877530cfd1d0b354d4628cc251c12ba7146a315 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Sat, 28 Jan 2017 17:32:55 -0800 Subject: [PATCH 18/23] Remove unnecessary functions --- .../components/camera/android_ip_webcam.py | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/homeassistant/components/camera/android_ip_webcam.py b/homeassistant/components/camera/android_ip_webcam.py index 87448d6338fec..aae4fb40f3e28 100644 --- a/homeassistant/components/camera/android_ip_webcam.py +++ b/homeassistant/components/camera/android_ip_webcam.py @@ -37,18 +37,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): yield from async_add_devices(cameras, True) -def extract_image_from_mjpeg(stream): - """Take in a MJPEG stream object, return the jpg from it.""" - data = b'' - for chunk in stream: - data += chunk - jpg_start = data.find(b'\xff\xd8') - jpg_end = data.find(b'\xff\xd9') - if jpg_start != -1 and jpg_end != -1: - jpg = data[jpg_start:jpg_end + 2] - return jpg - - class IPWebcamCamera(Camera): """An implementation of an IP camera that is reachable over a URL.""" @@ -92,18 +80,6 @@ def async_camera_image(self): if response is not None: yield from response.release() - def camera_image(self): - """Return a still image response from the camera.""" - if self._username and self._password: - auth = HTTPBasicAuth(self._username, self._password) - req = requests.get( - self._mjpeg_url, auth=auth, stream=True, timeout=10) - else: - req = requests.get(self._mjpeg_url, stream=True, timeout=10) - - with closing(req) as response: - return extract_image_from_mjpeg(response.iter_content(102400)) - @asyncio.coroutine def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" @@ -122,7 +98,3 @@ def name(self): def device_state_attributes(self): """Return the state attributes.""" return self._device.device_state_attributes - - def update(self): - """Update the state.""" - self._device.update() From 9e784ebaeb92c76a4c03ab37778227ac68e8c54c Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 2 Feb 2017 22:26:21 -0800 Subject: [PATCH 19/23] Initial async work --- homeassistant/components/android_ip_webcam.py | 121 +++++++++++------- .../binary_sensor/android_ip_webcam.py | 9 +- .../components/camera/android_ip_webcam.py | 3 - .../components/sensor/android_ip_webcam.py | 23 ++-- .../components/switch/android_ip_webcam.py | 5 +- 5 files changed, 94 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index dec545ca7ceb7..16683024c2cfc 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -4,21 +4,23 @@ 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 datetime, timedelta import xml.etree.ElementTree as ET from urllib.parse import quote -import requests +import aiohttp +import async_timeout import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery import homeassistant.util.dt as dt_util +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, CONF_SENSORS, CONF_SWITCHES) -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -144,8 +146,6 @@ ALLOWED_ORIENTATIONS = ['landscape', 'upsidedown', 'portrait', 'upsidedown_portrait'] -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=3) - def setup(hass, config): """Setup the IP Webcam component.""" @@ -154,7 +154,7 @@ def setup(hass, config): ip_webcam = hass.data.get(DATA_IP_WEBCAM) if ip_webcam is None: hass.data[DATA_IP_WEBCAM] = {} - hass.data[DATA_IP_WEBCAM][host] = IPWebcam(conf) + hass.data[DATA_IP_WEBCAM][host] = IPWebcam(hass, conf) if conf.get(CONF_MOTION_BINARY_SENSOR, False) is True: discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) @@ -173,8 +173,10 @@ def setup(hass, config): class IPWebcam(object): """The Android device running IP Webcam.""" - def __init__(self, config): + def __init__(self, hass, config): """Initialize the data oject.""" + self.hass = hass + self.websession = async_get_clientsession(hass) self._config = config self._name = self._config.get(CONF_NAME) self.host = self._config.get(CONF_HOST) @@ -184,41 +186,62 @@ def __init__(self, config): self.status_data = None self.sensor_data = None self._sensor_updated_at = (datetime.now() - timedelta(seconds=5)) - self.update() + self.async_update() @property def base_url(self): """Return the base url for endpoints.""" return 'http://{}:{}'.format(self.host, self.port) + @asyncio.coroutine def _request(self, path): """Make the actual request and return the parsed response.""" url = '{}{}'.format(self.base_url, path) - auth_tuple = () - - if self.username is not None and self.password is not None: - auth_tuple = (self.username, self.password) + auth = None if self.username is None else aiohttp.BasicAuth( + self.username, self.password) resp = 'json' if '.json' in path else 'xml' if '/startvideo' in path or '/stopvideo' in path: resp = 'json' + print('GET', url, 'AUTH', auth, 'RESP', resp) + + response = None + + data = None + + try: + with async_timeout.timeout(10, loop=self.hass.loop): + response = yield from self.websession.get(url, auth=auth) + + if response.status == 200: + if resp == 'xml': + data = yield from response.text() + elif resp == 'json': + data = yield from response.json() + except (asyncio.TimeoutError, + aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError) as error: + _LOGGER.error('Failed to communicate with IP Webcam: %s', + type(error)) + return False + finally: + if response is not None: + yield from response.release() + try: - response = requests.get(url, timeout=DEFAULT_TIMEOUT, - auth=auth_tuple) if resp == 'xml': - return ET.fromstring(response.text) - elif resp == 'json': - return response.json() - except requests.exceptions.HTTPError: - return {'device_state': 'error'} - except requests.exceptions.RequestException: - return {'device_state': 'offline'} - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + return ET.fromstring(data) + else: + return data + except AttributeError: + _LOGGER.error("Received invalid response: %s", data) + return False + + @asyncio.coroutine + def async_update(self): """Fetch the latest data from IP Webcam.""" self.status_data = self._request('/status.json') @@ -235,19 +258,21 @@ def name(self): def device_state_attributes(self): """Return the state attributes.""" state_attr = {} - state_attr[ATTR_VID_CONNS] = self.status_data.get('video_connections') - state_attr[ATTR_AUD_CONNS] = self.status_data.get('audio_connections') - for (key, val) in self.status_data.get('curvals').items(): - try: - val = float(val) - except ValueError: - val = val - - if val == 'on' or val == 'off': - val = (val == 'on') - - state_attr[KEY_MAP.get(key, key)] = val - return state_attr + if self.status_data is not None: + state_attr[ATTR_VID_CONNS] = self.status_data.get('video_connections') + state_attr[ATTR_AUD_CONNS] = self.status_data.get('audio_connections') + print('Self.status_data', self.status_data) + for (key, val) in self.status_data.get('curvals', {}).items(): + try: + val = float(val) + except ValueError: + val = val + + if val == 'on' or val == 'off': + val = (val == 'on') + + state_attr[KEY_MAP.get(key, key)] = val + return state_attr @property def enabled_sensors(self): @@ -258,17 +283,19 @@ def enabled_sensors(self): def current_settings(self): """Return a dictionary of the current settings.""" settings = {} - for (key, val) in self.status_data.get('curvals').items(): - try: - val = float(val) - except ValueError: - val = val - - if val == 'on' or val == 'off': - val = (val == 'on') - - settings[key] = val - return settings + if self.status_data is not None: + print('Self.status_data', self.status_data) + for (key, val) in self.status_data.get('curvals', {}).items(): + try: + val = float(val) + except ValueError: + val = val + + if val == 'on' or val == 'off': + val = (val == 'on') + + settings[key] = val + return settings def change_setting(self, key, val): """Change a setting.""" diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py index 08779842b0034..4270fb9cdee8e 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -50,10 +50,11 @@ def is_on(self): def update(self): """Retrieve latest state.""" - self._device.update() - container = self._device.sensor_data.get(self.variable) - data_point = container.get('data', [[0, [0.0]]]) - self._state = data_point[0][-1][0] == 1.0 + self._device.async_update() + if self._device.status_data is not None: + container = self._device.sensor_data.get(self.variable) + data_point = container.get('data', [[0, [0.0]]]) + self._state = data_point[0][-1][0] == 1.0 @property def icon(self): diff --git a/homeassistant/components/camera/android_ip_webcam.py b/homeassistant/components/camera/android_ip_webcam.py index aae4fb40f3e28..3aa7729f6b6ab 100644 --- a/homeassistant/components/camera/android_ip_webcam.py +++ b/homeassistant/components/camera/android_ip_webcam.py @@ -6,12 +6,9 @@ """ import asyncio import logging -from contextlib import closing import aiohttp import async_timeout -import requests -from requests.auth import HTTPBasicAuth from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.helpers.aiohttp_client import ( diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/sensor/android_ip_webcam.py index a021bd214f8d2..809bfd49d8441 100644 --- a/homeassistant/components/sensor/android_ip_webcam.py +++ b/homeassistant/components/sensor/android_ip_webcam.py @@ -65,16 +65,17 @@ def state(self): def update(self): """Retrieve latest state.""" - self._device.update() - if self.variable in ('audio_connections', 'video_connections'): - self._state = self._device.status_data.get(self.variable) - self._unit = 'Connections' - else: - container = self._device.sensor_data.get(self.variable) - 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._device.async_update() + if self._device.status_data is not None and self._device.sensor_data is not None: + if self.variable in ('audio_connections', 'video_connections'): + self._state = self._device.status_data.get(self.variable) + self._unit = 'Connections' + else: + container = self._device.sensor_data.get(self.variable) + 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 device_state_attributes(self): @@ -84,7 +85,7 @@ def device_state_attributes(self): @property def icon(self): """Return the icon for the sensor.""" - if self.variable == 'battery_level': + if self.variable == 'battery_level' and self._state is not None: rounded_level = round(int(self._state), -1) returning_icon = 'mdi:battery' if rounded_level < 10: diff --git a/homeassistant/components/switch/android_ip_webcam.py b/homeassistant/components/switch/android_ip_webcam.py index 142f1cd92f233..13d431edb7728 100644 --- a/homeassistant/components/switch/android_ip_webcam.py +++ b/homeassistant/components/switch/android_ip_webcam.py @@ -53,8 +53,9 @@ def name(self): def update(self): """Get the updated status of the switch.""" - self._device.update() - self._state = self._device.current_settings.get(self._setting) + self._device.async_update() + if self._device.status_data is not None: + self._state = self._device.current_settings.get(self._setting) @property def is_on(self): From cfb6618c08ebc4cf2dd33d738a553774fcf8b998 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 2 Feb 2017 22:46:13 -0800 Subject: [PATCH 20/23] Async fixes --- homeassistant/components/android_ip_webcam.py | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 16683024c2cfc..ac2623421cdcd 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -243,10 +243,11 @@ def _request(self, path): @asyncio.coroutine def async_update(self): """Fetch the latest data from IP Webcam.""" - self.status_data = self._request('/status.json') + self.status_data = yield from self._request('/status.json') utime = int(dt_util.as_timestamp(self._sensor_updated_at) * 1000) - self.sensor_data = self._request('/sensors.json?from={}'.format(utime)) + sensor_url = '/sensors.json?from={}' + self.sensor_data = yield from self._request(sensor_url.format(utime)) self._sensor_updated_at = datetime.now() @property @@ -259,8 +260,10 @@ def device_state_attributes(self): """Return the state attributes.""" state_attr = {} if self.status_data is not None: - state_attr[ATTR_VID_CONNS] = self.status_data.get('video_connections') - state_attr[ATTR_AUD_CONNS] = self.status_data.get('audio_connections') + vid_conns = 'video_connections' + aud_conns = 'audio_connections' + state_attr[ATTR_VID_CONNS] = self.status_data.get(vid_conns) + state_attr[ATTR_AUD_CONNS] = self.status_data.get(aud_conns) print('Self.status_data', self.status_data) for (key, val) in self.status_data.get('curvals', {}).items(): try: @@ -297,58 +300,71 @@ def current_settings(self): settings[key] = val return settings - def change_setting(self, key, val): + @asyncio.coroutine + def async_change_setting(self, key, val): """Change a setting.""" if isinstance(val, bool): payload = 'on' if val else 'off' else: payload = val - return self._request('/settings/{}?set={}'.format(key, payload)) + data = yield from self._request('/settings/{}?set={}'.format(key, + payload)) + return data def torch(self, activate=True): """Enable/disable the torch.""" path = '/enabletorch' if activate else '/disabletorch' - return self._request(path) + data = yield from self._request(path) + return data def focus(self, activate=True): """Enable/disable camera focus.""" path = '/focus' if activate else '/nofocus' - return self._request(path) + data = yield from self._request(path) + return data def record(self, record=True, tag=None): """Enable/disable recording.""" path = '/startvideo?force=1' if record else '/stopvideo?force=1' if record and tag is not None: path = '/startvideo?force=1&tag={}'.format(quote(tag)) - return self._request(path) + data = yield from self._request(path) + return data def set_front_facing_camera(self, activate=True): """Enable/disable the front-facing camera.""" - return self.change_setting('ffc', activate) + data = yield from self.async_change_setting('ffc', activate) + return data def set_night_vision(self, activate=True): """Enable/disable night vision.""" - return self.change_setting('night_vision', activate) + data = yield from self.async_change_setting('night_vision', activate) + return data def set_overlay(self, activate=True): """Enable/disable the video overlay.""" - return self.change_setting('overlay', activate) + data = yield from self.async_change_setting('overlay', activate) + return data def set_gps_active(self, activate=True): """Enable/disable GPS.""" - return self.change_setting('gps_active', activate) + data = yield from self.async_change_setting('gps_active', activate) + return data def set_quality(self, quality: int=100): """Set the video quality.""" - return self.change_setting('quality', quality) + data = yield from self.async_change_setting('quality', quality) + return data def set_orientation(self, orientation: str='landscape'): """Set the video orientation.""" if orientation not in ALLOWED_ORIENTATIONS: _LOGGER.debug('%s is not a valid orientation', orientation) return False - return self.change_setting('orientation', orientation) + data = yield from self.async_change_setting('orientation', orientation) + return data def set_zoom(self, zoom: int): """Set the zoom level.""" - return self._request('/settings/ptz?zoom={}'.format(zoom)) + data = yield from self._request('/settings/ptz?zoom={}'.format(zoom)) + return data From 1e073eecbc9df355f061e425403ec44c97b86737 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 2 Feb 2017 22:46:51 -0800 Subject: [PATCH 21/23] Line length fix --- homeassistant/components/sensor/android_ip_webcam.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/sensor/android_ip_webcam.py index 809bfd49d8441..0d5d0c72b6e85 100644 --- a/homeassistant/components/sensor/android_ip_webcam.py +++ b/homeassistant/components/sensor/android_ip_webcam.py @@ -66,7 +66,8 @@ def state(self): def update(self): """Retrieve latest state.""" self._device.async_update() - if self._device.status_data is not None and self._device.sensor_data is not None: + if (self._device.status_data is not None and + self._device.sensor_data is not None): if self.variable in ('audio_connections', 'video_connections'): self._state = self._device.status_data.get(self.variable) self._unit = 'Connections' From b90b33d45c34363b07d26e64f74f96f16b23e52c Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 2 Feb 2017 23:32:14 -0800 Subject: [PATCH 22/23] Remove stale prints --- homeassistant/components/android_ip_webcam.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index ac2623421cdcd..e43628e454832 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -206,8 +206,6 @@ def _request(self, path): if '/startvideo' in path or '/stopvideo' in path: resp = 'json' - print('GET', url, 'AUTH', auth, 'RESP', resp) - response = None data = None @@ -264,7 +262,6 @@ def device_state_attributes(self): aud_conns = 'audio_connections' state_attr[ATTR_VID_CONNS] = self.status_data.get(vid_conns) state_attr[ATTR_AUD_CONNS] = self.status_data.get(aud_conns) - print('Self.status_data', self.status_data) for (key, val) in self.status_data.get('curvals', {}).items(): try: val = float(val) @@ -287,7 +284,6 @@ def current_settings(self): """Return a dictionary of the current settings.""" settings = {} if self.status_data is not None: - print('Self.status_data', self.status_data) for (key, val) in self.status_data.get('curvals', {}).items(): try: val = float(val) From 98ba0ce1d004676af750b9169b468f38577163da Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 17 Feb 2017 22:39:26 -0800 Subject: [PATCH 23/23] Remove async_update --- homeassistant/components/android_ip_webcam.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index e43628e454832..777d962d7a125 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -186,7 +186,6 @@ def __init__(self, hass, config): self.status_data = None self.sensor_data = None self._sensor_updated_at = (datetime.now() - timedelta(seconds=5)) - self.async_update() @property def base_url(self):