From 573a446cfb0bf474850f4e55135da6db6c2d11d4 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 3 Nov 2018 00:06:15 +0000 Subject: [PATCH 01/37] change to webhook --- homeassistant/components/camera/push.py | 100 +++++++++--------------- 1 file changed, 38 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index c9deca1309d699..d4b6724e983127 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -13,27 +13,25 @@ from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\ STATE_IDLE, STATE_RECORDING from homeassistant.core import callback -from homeassistant.components.http.view import KEY_AUTHENTICATED,\ - HomeAssistantView -from homeassistant.const import CONF_NAME, CONF_TIMEOUT,\ - HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST +from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util +DEPENDENCIES = ['webhook'] + _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['http'] +RECEIVED_DATA = 'push_camera_received' CONF_BUFFER_SIZE = 'buffer' CONF_IMAGE_FIELD = 'field' -CONF_TOKEN = 'token' DEFAULT_NAME = "Push Camera" ATTR_FILENAME = 'filename' ATTR_LAST_TRIP = 'last_trip' -ATTR_TOKEN = 'token' +ATTR_WEBHOOK_URL = 'post_camera_to' PUSH_CAMERA_DATA = 'push_camera' @@ -43,7 +41,7 @@ vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All( cv.time_period, cv.positive_timedelta), vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string, - vol.Optional(CONF_TOKEN): vol.All(cv.string, vol.Length(min=8)), + vol.Required(CONF_WEBHOOK_ID): cv.string, }) @@ -53,69 +51,39 @@ async def async_setup_platform(hass, config, async_add_entities, if PUSH_CAMERA_DATA not in hass.data: hass.data[PUSH_CAMERA_DATA] = {} - cameras = [PushCamera(config[CONF_NAME], + webhook_id = config.get(CONF_WEBHOOK_ID) + + cameras = [PushCamera(hass, + config[CONF_NAME], config[CONF_BUFFER_SIZE], config[CONF_TIMEOUT], - config.get(CONF_TOKEN))] + config[CONF_IMAGE_FIELD], + webhook_id)] - hass.http.register_view(CameraPushReceiver(hass, - config[CONF_IMAGE_FIELD])) + try: + hass.components.webhook.async_register(webhook_id, handle_webhook) + except ValueError: + _LOGGER.error("In <%s>, webhook_id <%s> already used", + config[CONF_NAME], webhook_id) + return async_add_entities(cameras) -class CameraPushReceiver(HomeAssistantView): - """Handle pushes from remote camera.""" - - url = "/api/camera_push/{entity_id}" - name = 'api:camera_push:camera_entity' - requires_auth = False - - def __init__(self, hass, image_field): - """Initialize CameraPushReceiver with camera entity.""" - self._cameras = hass.data[PUSH_CAMERA_DATA] - self._image = image_field - - async def post(self, request, entity_id): - """Accept the POST from Camera.""" - _camera = self._cameras.get(entity_id) - - if _camera is None: - _LOGGER.error("Unknown %s", entity_id) - status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED]\ - else HTTP_UNAUTHORIZED - return self.json_message('Unknown {}'.format(entity_id), - status) - - # Supports HA authentication and token based - # when token has been configured - authenticated = (request[KEY_AUTHENTICATED] or - (_camera.token is not None and - request.query.get('token') == _camera.token)) - - if not authenticated: - return self.json_message( - 'Invalid authorization credentials for {}'.format(entity_id), - HTTP_UNAUTHORIZED) - - try: - data = await request.post() - _LOGGER.debug("Received Camera push: %s", data[self._image]) - await _camera.update_image(data[self._image].file.read(), - data[self._image].filename) - except ValueError as value_error: - _LOGGER.error("Unknown value %s", value_error) - return self.json_message('Invalid POST', HTTP_BAD_REQUEST) - except KeyError as key_error: - _LOGGER.error('In your POST message %s', key_error) - return self.json_message('{} missing'.format(self._image), - HTTP_BAD_REQUEST) +async def handle_webhook(hass, webhook_id, request): + """Handle incoming webhook POST with image files.""" + data = dict(await request.post()) + camera = hass.data[PUSH_CAMERA_DATA][webhook_id] + + await camera.update_image(data[camera.image_field].file.read(), + data[camera.image_field].filename) class PushCamera(Camera): """The representation of a Push camera.""" - def __init__(self, name, buffer_size, timeout, token): + def __init__(self, hass, name, buffer_size, timeout, image_field, + webhook_id): """Initialize push camera component.""" super().__init__() self._name = name @@ -126,11 +94,19 @@ def __init__(self, name, buffer_size, timeout, token): self._timeout = timeout self.queue = deque([], buffer_size) self._current_image = None - self.token = token + self._image_field = image_field + self.webhook_id = webhook_id + self.webhook_url = \ + hass.components.webhook.async_generate_url(webhook_id) async def async_added_to_hass(self): """Call when entity is added to hass.""" - self.hass.data[PUSH_CAMERA_DATA][self.entity_id] = self + self.hass.data[PUSH_CAMERA_DATA][self.webhook_id] = self + + @property + def image_field(self): + """HTTP field containing the image file.""" + return self._image_field @property def state(self): @@ -189,6 +165,6 @@ def device_state_attributes(self): name: value for name, value in ( (ATTR_LAST_TRIP, self._last_trip), (ATTR_FILENAME, self._filename), - (ATTR_TOKEN, self.token), + (ATTR_WEBHOOK_URL, self.webhook_url), ) if value is not None } From 090dc9c422175a2f3f7e9354b6048c996b0efef4 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 9 Nov 2018 23:44:41 +0000 Subject: [PATCH 02/37] dont persist webhook url in attributes --- homeassistant/components/camera/push.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index d4b6724e983127..3c7a940f644811 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -165,6 +165,5 @@ def device_state_attributes(self): name: value for name, value in ( (ATTR_LAST_TRIP, self._last_trip), (ATTR_FILENAME, self._filename), - (ATTR_WEBHOOK_URL, self.webhook_url), ) if value is not None } From 7c130817c4e723892ba3a58247c60c9beb188889 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 9 Nov 2018 23:47:59 +0000 Subject: [PATCH 03/37] remove orphan attribute --- homeassistant/components/camera/push.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index 3c7a940f644811..0d2cc6c6276e95 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -31,7 +31,6 @@ ATTR_FILENAME = 'filename' ATTR_LAST_TRIP = 'last_trip' -ATTR_WEBHOOK_URL = 'post_camera_to' PUSH_CAMERA_DATA = 'push_camera' From 64ada1ea5a51721ee453e4cf32b9a7e7e7324245 Mon Sep 17 00:00:00 2001 From: uchagani Date: Fri, 9 Nov 2018 20:04:28 -0500 Subject: [PATCH 04/37] bump total connect client to 0.22 (#18344) --- homeassistant/components/alarm_control_panel/totalconnect.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py index 2989bb1be37060..97f46cb0dfd4eb 100644 --- a/homeassistant/components/alarm_control_panel/totalconnect.py +++ b/homeassistant/components/alarm_control_panel/totalconnect.py @@ -18,7 +18,7 @@ STATE_ALARM_ARMED_CUSTOM_BYPASS) -REQUIREMENTS = ['total_connect_client==0.20'] +REQUIREMENTS = ['total_connect_client==0.22'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 354cf822a80fec..321c39118fa28a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1517,7 +1517,7 @@ todoist-python==7.0.17 toonlib==1.1.3 # homeassistant.components.alarm_control_panel.totalconnect -total_connect_client==0.20 +total_connect_client==0.22 # homeassistant.components.tplink_lte tp-connected==0.0.4 From 210eab16dadf72a610b69bd96d21dc61aabbdf62 Mon Sep 17 00:00:00 2001 From: Adam Belebczuk Date: Sat, 10 Nov 2018 02:17:24 -0500 Subject: [PATCH 05/37] WeMo - Change name of discovery option (#18348) --- homeassistant/components/wemo.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index ab2094ba9d73ef..93760405e08051 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -58,17 +58,17 @@ def coerce_host_port(value): CONF_STATIC = 'static' -CONF_DISABLE_DISCOVERY = 'disable_discovery' +CONF_DISCOVERY = 'discovery' -DEFAULT_DISABLE_DISCOVERY = False +DEFAULT_DISCOVERY = True CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_STATIC, default=[]): vol.Schema([ vol.All(cv.string, coerce_host_port) ]), - vol.Optional(CONF_DISABLE_DISCOVERY, - default=DEFAULT_DISABLE_DISCOVERY): cv.boolean + vol.Optional(CONF_DISCOVERY, + default=DEFAULT_DISCOVERY): cv.boolean }), }, extra=vol.ALLOW_EXTRA) @@ -141,9 +141,7 @@ def setup_url_for_address(host, port): devices.append((url, device)) - disable_discovery = config.get(DOMAIN, {}).get(CONF_DISABLE_DISCOVERY) - - if not disable_discovery: + if config.get(DOMAIN, {}).get(CONF_DISCOVERY): _LOGGER.debug("Scanning for WeMo devices.") devices.extend( (setup_url_for_device(device), device) From 8bd281d5a32bbac398d6bd06a6a7da7be74337af Mon Sep 17 00:00:00 2001 From: Tyler Page Date: Sat, 10 Nov 2018 01:21:39 -0600 Subject: [PATCH 06/37] Update credstash.py (#18349) * Update credstash.py * Update requirements_all.txt --- homeassistant/scripts/credstash.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/scripts/credstash.py b/homeassistant/scripts/credstash.py index 84ba20619d8f8e..302910c5b08109 100644 --- a/homeassistant/scripts/credstash.py +++ b/homeassistant/scripts/credstash.py @@ -4,7 +4,7 @@ from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['credstash==1.14.0', 'botocore==1.7.34'] +REQUIREMENTS = ['credstash==1.15.0', 'botocore==1.7.34'] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 321c39118fa28a..88b78be00117ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ concord232==0.15 construct==2.9.45 # homeassistant.scripts.credstash -# credstash==1.14.0 +# credstash==1.15.0 # homeassistant.components.sensor.crimereports crimereports==1.0.0 From 132bb7902a74bf6ac2724a114c581f901760424e Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 10 Nov 2018 18:33:45 +0100 Subject: [PATCH 07/37] Update HAP-python to 2.4.0 (#18355) --- homeassistant/components/homekit/__init__.py | 2 +- homeassistant/components/homekit/type_switches.py | 7 ++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index f8514a5d030b55..650fff45c7b804 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -29,7 +29,7 @@ from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) -REQUIREMENTS = ['HAP-python==2.2.2'] +REQUIREMENTS = ['HAP-python==2.4.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 553d74f5a527a1..b41e1a015432f8 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -2,7 +2,8 @@ import logging from pyhap.const import ( - CATEGORY_OUTLET, CATEGORY_SWITCH) + CATEGORY_FAUCET, CATEGORY_OUTLET, CATEGORY_SHOWER_HEAD, + CATEGORY_SPRINKLER, CATEGORY_SWITCH) from homeassistant.components.script import ATTR_CAN_CANCEL from homeassistant.components.switch import DOMAIN @@ -20,10 +21,6 @@ _LOGGER = logging.getLogger(__name__) -CATEGORY_SPRINKLER = 28 -CATEGORY_FAUCET = 29 -CATEGORY_SHOWER_HEAD = 30 - VALVE_TYPE = { TYPE_FAUCET: (CATEGORY_FAUCET, 3), TYPE_SHOWER: (CATEGORY_SHOWER_HEAD, 2), diff --git a/requirements_all.txt b/requirements_all.txt index 88b78be00117ca..1ec88c98fa0f04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -32,7 +32,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.0.0 # homeassistant.components.homekit -HAP-python==2.2.2 +HAP-python==2.4.0 # homeassistant.components.notify.mastodon Mastodon.py==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a6470fa474d79..4011be94bb662b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.5.2 # homeassistant.components.homekit -HAP-python==2.2.2 +HAP-python==2.4.0 # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.1.3 From e75f9b36f907b747bc364fdee86bdd14340561d0 Mon Sep 17 00:00:00 2001 From: Antoine Meillet Date: Sat, 10 Nov 2018 21:08:03 +0100 Subject: [PATCH 08/37] add heartbeat support to mysensors (#18359) --- homeassistant/components/mysensors/device.py | 2 ++ homeassistant/components/mysensors/handler.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 7f4f6100204c0d..07261b1c2a6b0e 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -15,6 +15,7 @@ ATTR_DESCRIPTION = 'description' ATTR_DEVICE = 'device' ATTR_NODE_ID = 'node_id' +ATTR_HEARTBEAT = 'heartbeat' MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' @@ -51,6 +52,7 @@ def device_state_attributes(self): child = node.children[self.child_id] attr = { ATTR_BATTERY_LEVEL: node.battery_level, + ATTR_HEARTBEAT: node.heartbeat, ATTR_CHILD_ID: self.child_id, ATTR_DESCRIPTION: child.description, ATTR_DEVICE: self.gateway.device, diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index 3403c5896396b6..39af11737065d2 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -45,6 +45,12 @@ async def handle_battery_level(hass, hass_config, msg): _handle_node_update(hass, msg) +@HANDLERS.register('I_HEARTBEAT_RESPONSE') +async def handle_heartbeat(hass, hass_config, msg): + """Handle an heartbeat.""" + _handle_node_update(hass, msg) + + @HANDLERS.register('I_SKETCH_NAME') async def handle_sketch_name(hass, hass_config, msg): """Handle an internal sketch name message.""" From f236e14bd6bd71554f1e1529d9c7a259cec55fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 10 Nov 2018 22:08:32 +0200 Subject: [PATCH 09/37] Upgrade pytest and pytest-sugar (#18338) * Upgrade pytest to 3.10.0 * Upgrade pytest-sugar to 0.9.2 --- requirements_test.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 68248a47cdb7cf..9f34d9ee861ad9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ pydocstyle==2.1.1 pylint==2.1.1 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 -pytest-sugar==0.9.1 +pytest-sugar==0.9.2 pytest-timeout==1.3.2 -pytest==3.9.3 +pytest==3.10.0 requests_mock==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4011be94bb662b..8fd96db4942d41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -12,9 +12,9 @@ pydocstyle==2.1.1 pylint==2.1.1 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 -pytest-sugar==0.9.1 +pytest-sugar==0.9.2 pytest-timeout==1.3.2 -pytest==3.9.3 +pytest==3.10.0 requests_mock==1.5.2 From 667b41dd4a81e3d41b5838859a3554b8fb1ba2ef Mon Sep 17 00:00:00 2001 From: Chris Kacerguis Date: Sat, 10 Nov 2018 14:30:03 -0600 Subject: [PATCH 10/37] Show battery_level as a percent vs a decimal (#18328) --- homeassistant/components/wirelesstag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wirelesstag.py b/homeassistant/components/wirelesstag.py index f2832100066152..77b4c48b41b798 100644 --- a/homeassistant/components/wirelesstag.py +++ b/homeassistant/components/wirelesstag.py @@ -271,7 +271,7 @@ def update(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_BATTERY_LEVEL: self._tag.battery_remaining, + ATTR_BATTERY_LEVEL: int(self._tag.battery_remaining*100), ATTR_VOLTAGE: '{:.2f}V'.format(self._tag.battery_volts), ATTR_TAG_SIGNAL_STRENGTH: '{}dBm'.format( self._tag.signal_strength), From fa127188df1c20c5fe34c8d514fe677ec28df99b Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 10 Nov 2018 12:17:14 -0800 Subject: [PATCH 11/37] Bump python-avion dependency The current version of python-avion doesn't work correctly with Python 3.5. Update it to one that does. --- homeassistant/components/light/avion.py | 2 +- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/avion.py b/homeassistant/components/light/avion.py index 731f0e600fb143..00fc4f337412ef 100644 --- a/homeassistant/components/light/avion.py +++ b/homeassistant/components/light/avion.py @@ -16,7 +16,7 @@ CONF_USERNAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['antsar-avion==0.9.1'] +REQUIREMENTS = ['avion==0.10'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1ec88c98fa0f04..77bb6646894042 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -140,9 +140,6 @@ anel_pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.media_player.anthemav anthemav==1.1.8 -# homeassistant.components.light.avion -# antsar-avion==0.9.1 - # homeassistant.components.apcupsd apcaccess==0.0.13 @@ -159,6 +156,9 @@ asterisk_mbox==0.5.0 # homeassistant.components.media_player.dlna_dmr async-upnp-client==0.13.1 +# homeassistant.components.light.avion +# avion==0.10 + # homeassistant.components.axis axis==16 From a85a0b0fcf51ed09a4172e6a92ec3f8fe3d16635 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 12:50:25 +0000 Subject: [PATCH 12/37] update tests --- tests/components/camera/test_push.py | 73 ++++++---------------------- 1 file changed, 14 insertions(+), 59 deletions(-) diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py index 6d9688c10e62ab..7d018a74c3e3c6 100644 --- a/tests/components/camera/test_push.py +++ b/tests/components/camera/test_push.py @@ -15,69 +15,24 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): 'camera': { 'platform': 'push', 'name': 'config_test', - 'token': '12345678' + 'webhook_id': 'camera.config_test' }}) client = await aiohttp_client(hass.http.app) - # missing file - resp = await client.post('/api/camera_push/camera.config_test') - assert resp.status == 400 - - # wrong entity + # wrong webhook files = {'image': io.BytesIO(b'fake')} resp = await client.post('/api/camera_push/camera.wrong', data=files) - assert resp.status == 404 - - -async def test_cases_with_no_auth(aioclient_mock, hass, aiohttp_client): - """Test cases where aiohttp_client is not auth.""" - await async_setup_component(hass, 'camera', { - 'camera': { - 'platform': 'push', - 'name': 'config_test', - 'token': '12345678' - }}) - - setup_auth(hass.http.app, [], True, api_password=None) - client = await aiohttp_client(hass.http.app) - - # wrong token - files = {'image': io.BytesIO(b'fake')} - resp = await client.post('/api/camera_push/camera.config_test?token=1234', - data=files) - assert resp.status == 401 - - # right token - files = {'image': io.BytesIO(b'fake')} - resp = await client.post( - '/api/camera_push/camera.config_test?token=12345678', - data=files) - assert resp.status == 200 - - -async def test_no_auth_no_token(aioclient_mock, hass, aiohttp_client): - """Test cases where aiohttp_client is not auth.""" - await async_setup_component(hass, 'camera', { - 'camera': { - 'platform': 'push', - 'name': 'config_test', - }}) - - setup_auth(hass.http.app, [], True, api_password=None) - client = await aiohttp_client(hass.http.app) - - # no token - files = {'image': io.BytesIO(b'fake')} - resp = await client.post('/api/camera_push/camera.config_test', - data=files) - assert resp.status == 401 + assert resp.status == 404 + + # missing file + camera_state = hass.states.get('camera.config_test') + assert camera_state.state == 'idle' + + resp = await client.post('/api/webhook/camera.config_test') + assert resp.status == 200 #webhooks always return 200 - # fake token - files = {'image': io.BytesIO(b'fake')} - resp = await client.post( - '/api/camera_push/camera.config_test?token=12345678', - data=files) - assert resp.status == 401 + camera_state = hass.states.get('camera.config_test') + assert camera_state.state == 'idle' #no file supplied we are still idle async def test_posting_url(hass, aiohttp_client): @@ -86,7 +41,7 @@ async def test_posting_url(hass, aiohttp_client): 'camera': { 'platform': 'push', 'name': 'config_test', - 'token': '12345678' + 'webhook_id': 'camera.config_test' }}) client = await aiohttp_client(hass.http.app) @@ -98,7 +53,7 @@ async def test_posting_url(hass, aiohttp_client): # post image resp = await client.post( - '/api/camera_push/camera.config_test?token=12345678', + '/api/webhook/camera.config_test', data=files) assert resp.status == 200 From a805a6e8657abc548a6e41f965510ceb1c0d8030 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 12:52:05 +0000 Subject: [PATCH 13/37] update tests --- tests/components/camera/test_push.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py index 7d018a74c3e3c6..405c38048d0787 100644 --- a/tests/components/camera/test_push.py +++ b/tests/components/camera/test_push.py @@ -6,7 +6,6 @@ from homeassistant import core as ha from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from homeassistant.components.http.auth import setup_auth async def test_bad_posting(aioclient_mock, hass, aiohttp_client): @@ -19,20 +18,20 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): }}) client = await aiohttp_client(hass.http.app) - # wrong webhook + # wrong webhook files = {'image': io.BytesIO(b'fake')} resp = await client.post('/api/camera_push/camera.wrong', data=files) - assert resp.status == 404 - + assert resp.status == 404 + # missing file camera_state = hass.states.get('camera.config_test') assert camera_state.state == 'idle' - + resp = await client.post('/api/webhook/camera.config_test') - assert resp.status == 200 #webhooks always return 200 + assert resp.status == 200 # webhooks always return 200 camera_state = hass.states.get('camera.config_test') - assert camera_state.state == 'idle' #no file supplied we are still idle + assert camera_state.state == 'idle' # no file supplied we are still idle async def test_posting_url(hass, aiohttp_client): From 82f9e862837fb5657427116d343993c032e8bb72 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 12:52:35 +0000 Subject: [PATCH 14/37] add warning --- homeassistant/components/camera/push.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index 0d2cc6c6276e95..617d0f9ba78b2a 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -73,6 +73,10 @@ async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook POST with image files.""" data = dict(await request.post()) camera = hass.data[PUSH_CAMERA_DATA][webhook_id] + + if camera.image_field not in data: + _LOGGER.warning("Webhook call without POST parameter <%s>", camera.image_field) + return await camera.update_image(data[camera.image_field].file.read(), data[camera.image_field].filename) From efd04feefcaf816950a2e7af97f1bd58b1a132a6 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 12:53:47 +0000 Subject: [PATCH 15/37] hound fixes --- homeassistant/components/camera/push.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index 617d0f9ba78b2a..f2e03a3b946131 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -73,9 +73,10 @@ async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook POST with image files.""" data = dict(await request.post()) camera = hass.data[PUSH_CAMERA_DATA][webhook_id] - + if camera.image_field not in data: - _LOGGER.warning("Webhook call without POST parameter <%s>", camera.image_field) + _LOGGER.warning("Webhook call without POST parameter <%s>", + camera.image_field) return await camera.update_image(data[camera.image_field].file.read(), From f0a0ce504b17cdefbf71c5ad01cdc08d35eb2090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 11 Nov 2018 14:06:21 +0100 Subject: [PATCH 16/37] Better error handling in Tibber (#18363) * Better error handling in Tibber * return if received error --- homeassistant/components/sensor/tibber.py | 12 ++++++++++-- homeassistant/components/tibber/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 1207c8dfe201fa..861fd6eff53e82 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -171,8 +171,16 @@ async def async_added_to_hass(self): async def _async_callback(self, payload): """Handle received data.""" - data = payload.get('data', {}) - live_measurement = data.get('liveMeasurement', {}) + errors = payload.get('errors') + if errors: + _LOGGER.error(errors[0]) + return + data = payload.get('data') + if data is None: + return + live_measurement = data.get('liveMeasurement') + if live_measurement is None: + return self._state = live_measurement.pop('power', None) self._device_state_attributes = live_measurement self.async_schedule_update_ha_state() diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 38605e949bb287..8e824c0e2c2aba 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['pyTibber==0.7.4'] +REQUIREMENTS = ['pyTibber==0.7.5'] DOMAIN = 'tibber' diff --git a/requirements_all.txt b/requirements_all.txt index 77bb6646894042..f9922bbf0395a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -812,7 +812,7 @@ pyRFXtrx==0.23 pySwitchmate==0.4.3 # homeassistant.components.tibber -pyTibber==0.7.4 +pyTibber==0.7.5 # homeassistant.components.switch.dlink pyW215==0.6.0 From 9f966d1366acbc7221f2ae6f5b02acd895ef4574 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 14:13:03 +0000 Subject: [PATCH 17/37] block till done --- tests/components/camera/test_push.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py index 405c38048d0787..610e0cd8892a61 100644 --- a/tests/components/camera/test_push.py +++ b/tests/components/camera/test_push.py @@ -16,6 +16,7 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): 'name': 'config_test', 'webhook_id': 'camera.config_test' }}) + await hass.async_block_till_done() client = await aiohttp_client(hass.http.app) # wrong webhook @@ -42,6 +43,7 @@ async def test_posting_url(hass, aiohttp_client): 'name': 'config_test', 'webhook_id': 'camera.config_test' }}) + await hass.async_block_till_done() client = await aiohttp_client(hass.http.app) files = {'image': io.BytesIO(b'fake')} From e210ae70ba0e109d63bbba4028d4d985585b1b3a Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 15:08:57 +0000 Subject: [PATCH 18/37] trying to fix travis test --- tests/components/camera/test_push.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py index 610e0cd8892a61..0de328e228ed8a 100644 --- a/tests/components/camera/test_push.py +++ b/tests/components/camera/test_push.py @@ -4,12 +4,14 @@ from datetime import timedelta from homeassistant import core as ha +from homeassistant.components import webhook from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util async def test_bad_posting(aioclient_mock, hass, aiohttp_client): """Test that posting to wrong api endpoint fails.""" + await async_setup_component(hass, webhook.DOMAIN, {}) await async_setup_component(hass, 'camera', { 'camera': { 'platform': 'push', @@ -21,7 +23,7 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): # wrong webhook files = {'image': io.BytesIO(b'fake')} - resp = await client.post('/api/camera_push/camera.wrong', data=files) + resp = await client.post('/api/webhood/camera.wrong', data=files) assert resp.status == 404 # missing file @@ -37,6 +39,7 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): async def test_posting_url(hass, aiohttp_client): """Test that posting to api endpoint works.""" + await async_setup_component(hass, webhook.DOMAIN, {}) await async_setup_component(hass, 'camera', { 'camera': { 'platform': 'push', From 9c92151ad16745c4c00df6655dccf9ea1bf25ee5 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 11 Nov 2018 16:10:03 +0100 Subject: [PATCH 19/37] Upgrade async_upnp_client to 0.13.2 (#18377) --- homeassistant/components/media_player/dlna_dmr.py | 2 +- homeassistant/components/upnp/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index 5869cf2dbf963c..941b8844f864dc 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -26,7 +26,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import get_local_ip -REQUIREMENTS = ['async-upnp-client==0.13.1'] +REQUIREMENTS = ['async-upnp-client==0.13.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 1651879fd2c948..925ca561eb9ee2 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -29,7 +29,7 @@ from .device import Device -REQUIREMENTS = ['async-upnp-client==0.13.1'] +REQUIREMENTS = ['async-upnp-client==0.13.2'] NOTIFICATION_ID = 'upnp_notification' NOTIFICATION_TITLE = 'UPnP/IGD Setup' diff --git a/requirements_all.txt b/requirements_all.txt index f9922bbf0395a3..e5ef1ce8410b70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -154,7 +154,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp # homeassistant.components.media_player.dlna_dmr -async-upnp-client==0.13.1 +async-upnp-client==0.13.2 # homeassistant.components.light.avion # avion==0.10 From 47f3bad01cd8ba4a05ac9a3d714aeffe12aba54f Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 15:13:47 +0000 Subject: [PATCH 20/37] trying to fix travis test --- tests/components/camera/test_push.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py index 0de328e228ed8a..be1d24ce34fbb4 100644 --- a/tests/components/camera/test_push.py +++ b/tests/components/camera/test_push.py @@ -19,6 +19,8 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): 'webhook_id': 'camera.config_test' }}) await hass.async_block_till_done() + assert hass.states.get('camera.config_test') is not None + client = await aiohttp_client(hass.http.app) # wrong webhook From b8c06ad019a1a97e434d0bd703f07b98cdc748aa Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 11 Nov 2018 17:15:58 +0100 Subject: [PATCH 21/37] Fix including from sub dir (#18378) The include path is now always relative to the root of the config dir. --- homeassistant/util/ruamel_yaml.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index eb3e935c6ced39..8211252a516d21 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -80,7 +80,8 @@ def load_yaml(fname: str, round_trip: bool = False) -> JSON_TYPE: yaml = YAML(typ='rt') yaml.preserve_quotes = True else: - ExtSafeConstructor.name = fname + if not hasattr(ExtSafeConstructor, 'name'): + ExtSafeConstructor.name = fname yaml = YAML(typ='safe') yaml.Constructor = ExtSafeConstructor From 9411fca955a4ba301b7b8b0741b018fcad27873a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 11 Nov 2018 18:39:50 +0200 Subject: [PATCH 22/37] Add more type hints to helpers (#18350) * Add type hints to helpers.entityfilter * Add type hints to helpers.deprecation --- homeassistant/helpers/deprecation.py | 13 +++++++------ homeassistant/helpers/entityfilter.py | 21 ++++++++++++--------- tox.ini | 2 +- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 8b621b2f01c692..6ed7cbb9b516d0 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -1,9 +1,10 @@ """Deprecation helpers for Home Assistant.""" import inspect import logging +from typing import Any, Callable, Dict, Optional -def deprecated_substitute(substitute_name): +def deprecated_substitute(substitute_name: str) -> Callable[..., Callable]: """Help migrate properties to new names. When a property is added to replace an older property, this decorator can @@ -11,9 +12,9 @@ def deprecated_substitute(substitute_name): If the old property is defined, its value will be used instead, and a log warning will be issued alerting the user of the impending change. """ - def decorator(func): + def decorator(func: Callable) -> Callable: """Decorate function as deprecated.""" - def func_wrapper(self): + def func_wrapper(self: Callable) -> Any: """Wrap for the original function.""" if hasattr(self, substitute_name): # If this platform is still using the old property, issue @@ -28,8 +29,7 @@ def func_wrapper(self): substitute_name, substitute_name, func.__name__, inspect.getfile(self.__class__)) warnings[module_name] = True - # pylint: disable=protected-access - func._deprecated_substitute_warnings = warnings + setattr(func, '_deprecated_substitute_warnings', warnings) # Return the old property return getattr(self, substitute_name) @@ -38,7 +38,8 @@ def func_wrapper(self): return decorator -def get_deprecated(config, new_name, old_name, default=None): +def get_deprecated(config: Dict[str, Any], new_name: str, old_name: str, + default: Optional[Any] = None) -> Optional[Any]: """Allow an old config name to be deprecated with a replacement. If the new config isn't found, but the old one is, the old value is used diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index 141fc912275a15..7db577dfdc6a42 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -1,4 +1,5 @@ """Helper class to implement include/exclude of entities and domains.""" +from typing import Callable, Dict, Iterable import voluptuous as vol @@ -11,14 +12,14 @@ CONF_EXCLUDE_ENTITIES = 'exclude_entities' -def _convert_filter(config): +def _convert_filter(config: Dict[str, Iterable[str]]) -> Callable[[str], bool]: filt = generate_filter( config[CONF_INCLUDE_DOMAINS], config[CONF_INCLUDE_ENTITIES], config[CONF_EXCLUDE_DOMAINS], config[CONF_EXCLUDE_ENTITIES], ) - filt.config = config + setattr(filt, 'config', config) return filt @@ -33,8 +34,10 @@ def _convert_filter(config): }), _convert_filter) -def generate_filter(include_domains, include_entities, - exclude_domains, exclude_entities): +def generate_filter(include_domains: Iterable[str], + include_entities: Iterable[str], + exclude_domains: Iterable[str], + exclude_entities: Iterable[str]) -> Callable[[str], bool]: """Return a function that will filter entities based on the args.""" include_d = set(include_domains) include_e = set(include_entities) @@ -50,7 +53,7 @@ def generate_filter(include_domains, include_entities, # Case 2 - includes, no excludes - only include specified entities if have_include and not have_exclude: - def entity_filter_2(entity_id): + def entity_filter_2(entity_id: str) -> bool: """Return filter function for case 2.""" domain = split_entity_id(entity_id)[0] return (entity_id in include_e or @@ -60,7 +63,7 @@ def entity_filter_2(entity_id): # Case 3 - excludes, no includes - only exclude specified entities if not have_include and have_exclude: - def entity_filter_3(entity_id): + def entity_filter_3(entity_id: str) -> bool: """Return filter function for case 3.""" domain = split_entity_id(entity_id)[0] return (entity_id not in exclude_e and @@ -75,7 +78,7 @@ def entity_filter_3(entity_id): # note: if both include and exclude domains specified, # the exclude domains are ignored if include_d: - def entity_filter_4a(entity_id): + def entity_filter_4a(entity_id: str) -> bool: """Return filter function for case 4a.""" domain = split_entity_id(entity_id)[0] if domain in include_d: @@ -88,7 +91,7 @@ def entity_filter_4a(entity_id): # - if domain is excluded, pass if entity is included # - if domain is not excluded, pass if entity not excluded if exclude_d: - def entity_filter_4b(entity_id): + def entity_filter_4b(entity_id: str) -> bool: """Return filter function for case 4b.""" domain = split_entity_id(entity_id)[0] if domain in exclude_d: @@ -99,7 +102,7 @@ def entity_filter_4b(entity_id): # Case 4c - neither include or exclude domain specified # - Only pass if entity is included. Ignore entity excludes. - def entity_filter_4c(entity_id): + def entity_filter_4c(entity_id: str) -> bool: """Return filter function for case 4c.""" return entity_id in include_e diff --git a/tox.ini b/tox.ini index f5dee78893fae6..1ab771ff24b9f6 100644 --- a/tox.ini +++ b/tox.ini @@ -60,4 +60,4 @@ whitelist_externals=/bin/bash deps = -r{toxinidir}/requirements_test.txt commands = - /bin/bash -c 'mypy homeassistant/*.py homeassistant/{auth,util}/ homeassistant/helpers/{__init__,dispatcher,entity_values,icon,intent,json,location,signal,state,sun,temperature,translation,typing}.py' + /bin/bash -c 'mypy homeassistant/*.py homeassistant/{auth,util}/ homeassistant/helpers/{__init__,deprecation,dispatcher,entity_values,entityfilter,icon,intent,json,location,signal,state,sun,temperature,translation,typing}.py' From 5b9a9d8e04afa4aecf99925f675b13f74852ff66 Mon Sep 17 00:00:00 2001 From: Jack Wilsdon Date: Sun, 11 Nov 2018 16:43:01 +0000 Subject: [PATCH 23/37] Return color information in Alexa Smart Home response (#18368) Fixes #18367. --- homeassistant/components/alexa/smart_home.py | 20 +++++++++++++++ tests/components/alexa/test_smart_home.py | 26 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 99d2a50bee0f3b..9496bf6804cd30 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -474,6 +474,26 @@ class _AlexaColorController(_AlexaInterface): def name(self): return 'Alexa.ColorController' + def properties_supported(self): + return [{'name': 'color'}] + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name != 'color': + raise _UnsupportedProperty(name) + + hue, saturation = self.entity.attributes.get( + light.ATTR_HS_COLOR, (0, 0)) + + return { + 'hue': hue, + 'saturation': saturation / 100.0, + 'brightness': self.entity.attributes.get( + light.ATTR_BRIGHTNESS, 0) / 255.0, + } + class _AlexaColorTemperatureController(_AlexaInterface): """Implements Alexa.ColorTemperatureController. diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 4ea06b57a3890e..766075f8eb53f7 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1328,6 +1328,32 @@ async def test_report_dimmable_light_state(hass): properties.assert_equal('Alexa.BrightnessController', 'brightness', 0) +async def test_report_colored_light_state(hass): + """Test ColorController reports color correctly.""" + hass.states.async_set( + 'light.test_on', 'on', {'friendly_name': "Test light On", + 'hs_color': (180, 75), + 'brightness': 128, + 'supported_features': 17}) + hass.states.async_set( + 'light.test_off', 'off', {'friendly_name': "Test light Off", + 'supported_features': 17}) + + properties = await reported_properties(hass, 'light.test_on') + properties.assert_equal('Alexa.ColorController', 'color', { + 'hue': 180, + 'saturation': 0.75, + 'brightness': 128 / 255.0, + }) + + properties = await reported_properties(hass, 'light.test_off') + properties.assert_equal('Alexa.ColorController', 'color', { + 'hue': 0, + 'saturation': 0, + 'brightness': 0, + }) + + async def reported_properties(hass, endpoint): """Use ReportState to get properties and return them. From 9cb6464c587076e581233e3b009adc71837abc42 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Sun, 11 Nov 2018 11:44:41 -0500 Subject: [PATCH 24/37] catch key error when saving image (#18365) --- homeassistant/components/image_processing/tensorflow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/image_processing/tensorflow.py b/homeassistant/components/image_processing/tensorflow.py index a2cd997bb76a12..2d06dbbcf34e0c 100644 --- a/homeassistant/components/image_processing/tensorflow.py +++ b/homeassistant/components/image_processing/tensorflow.py @@ -246,7 +246,8 @@ def _save_image(self, image, matches, paths): for category, values in matches.items(): # Draw custom category regions/areas - if self._category_areas[category] != [0, 0, 1, 1]: + if (category in self._category_areas + and self._category_areas[category] != [0, 0, 1, 1]): label = "{} Detection Area".format(category.capitalize()) draw_box(draw, self._category_areas[category], img_width, img_height, label, (0, 255, 0)) From 02cc6a2f9a564b7a021c6e7f84e397000bd38738 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 11 Nov 2018 17:46:28 +0100 Subject: [PATCH 25/37] Fix hangouts notify (#18372) * Remove notify schema from hangouts platform * Notify platforms shouldn't overwrite the notify component service schema. That has no effect. * Fix hangouts service data key value --- homeassistant/components/notify/hangouts.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/notify/hangouts.py b/homeassistant/components/notify/hangouts.py index 01f98146f4c65f..7261663b99fc78 100644 --- a/homeassistant/components/notify/hangouts.py +++ b/homeassistant/components/notify/hangouts.py @@ -9,13 +9,12 @@ import voluptuous as vol from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA, - NOTIFY_SERVICE_SCHEMA, BaseNotificationService, ATTR_MESSAGE, ATTR_DATA) from homeassistant.components.hangouts.const \ - import (DOMAIN, SERVICE_SEND_MESSAGE, MESSAGE_DATA_SCHEMA, - TARGETS_SCHEMA, CONF_DEFAULT_CONVERSATIONS) + import (DOMAIN, SERVICE_SEND_MESSAGE, TARGETS_SCHEMA, + CONF_DEFAULT_CONVERSATIONS) _LOGGER = logging.getLogger(__name__) @@ -25,11 +24,6 @@ vol.Required(CONF_DEFAULT_CONVERSATIONS): [TARGETS_SCHEMA] }) -NOTIFY_SERVICE_SCHEMA = NOTIFY_SERVICE_SCHEMA.extend({ - vol.Optional(ATTR_TARGET): [TARGETS_SCHEMA], - vol.Optional(ATTR_DATA, default={}): MESSAGE_DATA_SCHEMA -}) - def get_service(hass, config, discovery_info=None): """Get the Hangouts notification service.""" @@ -61,8 +55,9 @@ def send_message(self, message="", **kwargs): service_data = { ATTR_TARGET: target_conversations, ATTR_MESSAGE: messages, - ATTR_DATA: kwargs[ATTR_DATA] } + if kwargs[ATTR_DATA]: + service_data[ATTR_DATA] = kwargs[ATTR_DATA] return self.hass.services.call( DOMAIN, SERVICE_SEND_MESSAGE, service_data=service_data) From 372470f52aecfdbd85ed7654d76d314014d46e8d Mon Sep 17 00:00:00 2001 From: bouni Date: Sun, 11 Nov 2018 17:48:44 +0100 Subject: [PATCH 26/37] Fix and improvment of Swiss Hydrological Data component (#17166) * Fix and improvment of Swiss Hydrological Data component * changed component to get data from a REST API rather than from crawling the website * fixed several issues and lint errors * Fix and improvment of Swiss Hydrological Data component * Minor changes - Simplify the sensor configuration (expose value as attributes rather than sensor) - Make the setup fail if station is not available - Add unique ID - Prepare for config flow --- .../sensor/swiss_hydrological_data.py | 231 +++++++++--------- requirements_all.txt | 4 +- 2 files changed, 113 insertions(+), 122 deletions(-) diff --git a/homeassistant/components/sensor/swiss_hydrological_data.py b/homeassistant/components/sensor/swiss_hydrological_data.py index fb55c22b2e8d9f..c354ebedb2b68e 100644 --- a/homeassistant/components/sensor/swiss_hydrological_data.py +++ b/homeassistant/components/sensor/swiss_hydrological_data.py @@ -4,145 +4,160 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.swiss_hydrological_data/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol -import requests from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - TEMP_CELSIUS, CONF_NAME, STATE_UNKNOWN, ATTR_ATTRIBUTION) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['xmltodict==0.11.0'] +REQUIREMENTS = ['swisshydrodata==0.0.3'] _LOGGER = logging.getLogger(__name__) -_RESOURCE = 'http://www.hydrodata.ch/xml/SMS.xml' - -CONF_STATION = 'station' -CONF_ATTRIBUTION = "Data provided by the Swiss Federal Office for the " \ - "Environment FOEN" - -DEFAULT_NAME = 'Water temperature' -ICON = 'mdi:cup-water' +ATTRIBUTION = "Data provided by the Swiss Federal Office for the " \ + "Environment FOEN" + +ATTR_DELTA_24H = 'delta-24h' +ATTR_MAX_1H = 'max-1h' +ATTR_MAX_24H = 'max-24h' +ATTR_MEAN_1H = 'mean-1h' +ATTR_MEAN_24H = 'mean-24h' +ATTR_MIN_1H = 'min-1h' +ATTR_MIN_24H = 'min-24h' +ATTR_PREVIOUS_24H = 'previous-24h' +ATTR_STATION = 'station' +ATTR_STATION_UPDATE = 'station_update' +ATTR_WATER_BODY = 'water_body' +ATTR_WATER_BODY_TYPE = 'water_body_type' -ATTR_LOCATION = 'location' -ATTR_UPDATE = 'update' -ATTR_DISCHARGE = 'discharge' -ATTR_WATERLEVEL = 'level' -ATTR_DISCHARGE_MEAN = 'discharge_mean' -ATTR_WATERLEVEL_MEAN = 'level_mean' -ATTR_TEMPERATURE_MEAN = 'temperature_mean' -ATTR_DISCHARGE_MAX = 'discharge_max' -ATTR_WATERLEVEL_MAX = 'level_max' -ATTR_TEMPERATURE_MAX = 'temperature_max' +CONF_STATION = 'station' -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +SENSOR_DISCHARGE = 'discharge' +SENSOR_LEVEL = 'level' +SENSOR_TEMPERATURE = 'temperature' + +CONDITIONS = { + SENSOR_DISCHARGE: 'mdi:waves', + SENSOR_LEVEL: 'mdi:zodiac-aquarius', + SENSOR_TEMPERATURE: 'mdi:oil-temperature', +} + +CONDITION_DETAILS = [ + ATTR_DELTA_24H, + ATTR_MAX_1H, + ATTR_MAX_24H, + ATTR_MEAN_1H, + ATTR_MEAN_24H, + ATTR_MIN_1H, + ATTR_MIN_24H, + ATTR_PREVIOUS_24H, +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_STATION): vol.Coerce(int), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=[SENSOR_TEMPERATURE]): + vol.All(cv.ensure_list, [vol.In(CONDITIONS)]), }) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Swiss hydrological sensor.""" - import xmltodict - - name = config.get(CONF_NAME) station = config.get(CONF_STATION) + monitored_conditions = config.get(CONF_MONITORED_CONDITIONS) + + hydro_data = HydrologicalData(station) + hydro_data.update() + + if hydro_data.data is None: + _LOGGER.error("The station doesn't exists: %s", station) + return + + entities = [] - try: - response = requests.get(_RESOURCE, timeout=5) - if any(str(station) == location.get('@StrNr') for location in - xmltodict.parse(response.text)['AKT_Data']['MesPar']) is False: - _LOGGER.error("The given station does not exist: %s", station) - return False - except requests.exceptions.ConnectionError: - _LOGGER.error("The URL is not accessible") - return False + for condition in monitored_conditions: + entities.append( + SwissHydrologicalDataSensor(hydro_data, station, condition)) - data = HydrologicalData(station) - add_entities([SwissHydrologicalDataSensor(name, data)], True) + add_entities(entities, True) class SwissHydrologicalDataSensor(Entity): - """Implementation of an Swiss hydrological sensor.""" + """Implementation of a Swiss hydrological sensor.""" - def __init__(self, name, data): - """Initialize the sensor.""" - self.data = data - self._name = name - self._unit_of_measurement = TEMP_CELSIUS - self._state = None + def __init__(self, hydro_data, station, condition): + """Initialize the Swiss hydrological sensor.""" + self.hydro_data = hydro_data + self._condition = condition + self._data = self._state = self._unit_of_measurement = None + self._icon = CONDITIONS[condition] + self._station = station @property def name(self): """Return the name of the sensor.""" - return self._name + return "{0} {1}".format(self._data['water-body-name'], self._condition) + + @property + def unique_id(self) -> str: + """Return a unique, friendly identifier for this entity.""" + return '{0}_{1}'.format(self._station, self._condition) @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - if self._state is not STATE_UNKNOWN: - return self._unit_of_measurement + if self._state is not None: + return self.hydro_data.data['parameters'][self._condition]['unit'] return None @property def state(self): """Return the state of the sensor.""" - try: - return round(float(self._state), 1) - except ValueError: - return STATE_UNKNOWN + if isinstance(self._state, (int, float)): + return round(self._state, 2) + return None @property def device_state_attributes(self): - """Return the state attributes.""" - attributes = {} - if self.data.measurings is not None: - if '02' in self.data.measurings: - attributes[ATTR_WATERLEVEL] = self.data.measurings['02'][ - 'current'] - attributes[ATTR_WATERLEVEL_MEAN] = self.data.measurings['02'][ - 'mean'] - attributes[ATTR_WATERLEVEL_MAX] = self.data.measurings['02'][ - 'max'] - if '03' in self.data.measurings: - attributes[ATTR_TEMPERATURE_MEAN] = self.data.measurings['03'][ - 'mean'] - attributes[ATTR_TEMPERATURE_MAX] = self.data.measurings['03'][ - 'max'] - if '10' in self.data.measurings: - attributes[ATTR_DISCHARGE] = self.data.measurings['10'][ - 'current'] - attributes[ATTR_DISCHARGE_MEAN] = self.data.measurings['10'][ - 'current'] - attributes[ATTR_DISCHARGE_MAX] = self.data.measurings['10'][ - 'max'] - - attributes[ATTR_LOCATION] = self.data.measurings['location'] - attributes[ATTR_UPDATE] = self.data.measurings['update_time'] - attributes[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION - return attributes + """Return the device state attributes.""" + attrs = {} + + if not self._data: + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION + return attrs + + attrs[ATTR_WATER_BODY_TYPE] = self._data['water-body-type'] + attrs[ATTR_STATION] = self._data['name'] + attrs[ATTR_STATION_UPDATE] = \ + self._data['parameters'][self._condition]['datetime'] + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION + + for entry in CONDITION_DETAILS: + attrs[entry.replace('-', '_')] = \ + self._data['parameters'][self._condition][entry] + + return attrs @property def icon(self): - """Icon to use in the frontend, if any.""" - return ICON + """Icon to use in the frontend.""" + return self._icon def update(self): - """Get the latest data and update the states.""" - self.data.update() - if self.data.measurings is not None: - if '03' not in self.data.measurings: - self._state = STATE_UNKNOWN - else: - self._state = self.data.measurings['03']['current'] + """Get the latest data and update the state.""" + self.hydro_data.update() + self._data = self.hydro_data.data + + if self._data is None: + self._state = None + else: + self._state = self._data['parameters'][self._condition]['value'] class HydrologicalData: @@ -151,38 +166,12 @@ class HydrologicalData: def __init__(self, station): """Initialize the data object.""" self.station = station - self.measurings = None + self.data = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Get the latest data from hydrodata.ch.""" - import xmltodict - - details = {} - try: - response = requests.get(_RESOURCE, timeout=5) - except requests.exceptions.ConnectionError: - _LOGGER.error("Unable to retrieve data from %s", _RESOURCE) - - try: - stations = xmltodict.parse(response.text)['AKT_Data']['MesPar'] - # Water level: Typ="02", temperature: Typ="03", discharge: Typ="10" - for station in stations: - if str(self.station) != station.get('@StrNr'): - continue - for data in ['02', '03', '10']: - if data != station.get('@Typ'): - continue - values = station.get('Wert') - if values is not None: - details[data] = { - 'current': values[0], - 'max': list(values[4].items())[1][1], - 'mean': list(values[3].items())[1][1]} - - details['location'] = station.get('Name') - details['update_time'] = station.get('Zeit') - - self.measurings = details - except AttributeError: - self.measurings = None + """Get the latest data.""" + from swisshydrodata import SwissHydroData + + shd = SwissHydroData() + self.data = shd.get_station(self.station) diff --git a/requirements_all.txt b/requirements_all.txt index e5ef1ce8410b70..459fd365c08a07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1474,6 +1474,9 @@ suds-passworddigest-homeassistant==0.1.2a0.dev0 # homeassistant.components.camera.onvif suds-py3==1.3.3.0 +# homeassistant.components.sensor.swiss_hydrological_data +swisshydrodata==0.0.3 + # homeassistant.components.tahoma tahoma-api==0.0.13 @@ -1606,7 +1609,6 @@ xknx==0.9.1 # homeassistant.components.media_player.bluesound # homeassistant.components.sensor.startca -# homeassistant.components.sensor.swiss_hydrological_data # homeassistant.components.sensor.ted5000 # homeassistant.components.sensor.yr # homeassistant.components.sensor.zestimate From 81e0e34da73f253eb7c42172a78041b274e6e1d6 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 3 Nov 2018 00:06:15 +0000 Subject: [PATCH 27/37] change to webhook --- homeassistant/components/camera/push.py | 100 +++++++++--------------- 1 file changed, 38 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index c9deca1309d699..d4b6724e983127 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -13,27 +13,25 @@ from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\ STATE_IDLE, STATE_RECORDING from homeassistant.core import callback -from homeassistant.components.http.view import KEY_AUTHENTICATED,\ - HomeAssistantView -from homeassistant.const import CONF_NAME, CONF_TIMEOUT,\ - HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST +from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util +DEPENDENCIES = ['webhook'] + _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['http'] +RECEIVED_DATA = 'push_camera_received' CONF_BUFFER_SIZE = 'buffer' CONF_IMAGE_FIELD = 'field' -CONF_TOKEN = 'token' DEFAULT_NAME = "Push Camera" ATTR_FILENAME = 'filename' ATTR_LAST_TRIP = 'last_trip' -ATTR_TOKEN = 'token' +ATTR_WEBHOOK_URL = 'post_camera_to' PUSH_CAMERA_DATA = 'push_camera' @@ -43,7 +41,7 @@ vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All( cv.time_period, cv.positive_timedelta), vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string, - vol.Optional(CONF_TOKEN): vol.All(cv.string, vol.Length(min=8)), + vol.Required(CONF_WEBHOOK_ID): cv.string, }) @@ -53,69 +51,39 @@ async def async_setup_platform(hass, config, async_add_entities, if PUSH_CAMERA_DATA not in hass.data: hass.data[PUSH_CAMERA_DATA] = {} - cameras = [PushCamera(config[CONF_NAME], + webhook_id = config.get(CONF_WEBHOOK_ID) + + cameras = [PushCamera(hass, + config[CONF_NAME], config[CONF_BUFFER_SIZE], config[CONF_TIMEOUT], - config.get(CONF_TOKEN))] + config[CONF_IMAGE_FIELD], + webhook_id)] - hass.http.register_view(CameraPushReceiver(hass, - config[CONF_IMAGE_FIELD])) + try: + hass.components.webhook.async_register(webhook_id, handle_webhook) + except ValueError: + _LOGGER.error("In <%s>, webhook_id <%s> already used", + config[CONF_NAME], webhook_id) + return async_add_entities(cameras) -class CameraPushReceiver(HomeAssistantView): - """Handle pushes from remote camera.""" - - url = "/api/camera_push/{entity_id}" - name = 'api:camera_push:camera_entity' - requires_auth = False - - def __init__(self, hass, image_field): - """Initialize CameraPushReceiver with camera entity.""" - self._cameras = hass.data[PUSH_CAMERA_DATA] - self._image = image_field - - async def post(self, request, entity_id): - """Accept the POST from Camera.""" - _camera = self._cameras.get(entity_id) - - if _camera is None: - _LOGGER.error("Unknown %s", entity_id) - status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED]\ - else HTTP_UNAUTHORIZED - return self.json_message('Unknown {}'.format(entity_id), - status) - - # Supports HA authentication and token based - # when token has been configured - authenticated = (request[KEY_AUTHENTICATED] or - (_camera.token is not None and - request.query.get('token') == _camera.token)) - - if not authenticated: - return self.json_message( - 'Invalid authorization credentials for {}'.format(entity_id), - HTTP_UNAUTHORIZED) - - try: - data = await request.post() - _LOGGER.debug("Received Camera push: %s", data[self._image]) - await _camera.update_image(data[self._image].file.read(), - data[self._image].filename) - except ValueError as value_error: - _LOGGER.error("Unknown value %s", value_error) - return self.json_message('Invalid POST', HTTP_BAD_REQUEST) - except KeyError as key_error: - _LOGGER.error('In your POST message %s', key_error) - return self.json_message('{} missing'.format(self._image), - HTTP_BAD_REQUEST) +async def handle_webhook(hass, webhook_id, request): + """Handle incoming webhook POST with image files.""" + data = dict(await request.post()) + camera = hass.data[PUSH_CAMERA_DATA][webhook_id] + + await camera.update_image(data[camera.image_field].file.read(), + data[camera.image_field].filename) class PushCamera(Camera): """The representation of a Push camera.""" - def __init__(self, name, buffer_size, timeout, token): + def __init__(self, hass, name, buffer_size, timeout, image_field, + webhook_id): """Initialize push camera component.""" super().__init__() self._name = name @@ -126,11 +94,19 @@ def __init__(self, name, buffer_size, timeout, token): self._timeout = timeout self.queue = deque([], buffer_size) self._current_image = None - self.token = token + self._image_field = image_field + self.webhook_id = webhook_id + self.webhook_url = \ + hass.components.webhook.async_generate_url(webhook_id) async def async_added_to_hass(self): """Call when entity is added to hass.""" - self.hass.data[PUSH_CAMERA_DATA][self.entity_id] = self + self.hass.data[PUSH_CAMERA_DATA][self.webhook_id] = self + + @property + def image_field(self): + """HTTP field containing the image file.""" + return self._image_field @property def state(self): @@ -189,6 +165,6 @@ def device_state_attributes(self): name: value for name, value in ( (ATTR_LAST_TRIP, self._last_trip), (ATTR_FILENAME, self._filename), - (ATTR_TOKEN, self.token), + (ATTR_WEBHOOK_URL, self.webhook_url), ) if value is not None } From 01de2b83ab177924e60de76e100c0235eef62881 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 9 Nov 2018 23:44:41 +0000 Subject: [PATCH 28/37] dont persist webhook url in attributes --- homeassistant/components/camera/push.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index d4b6724e983127..3c7a940f644811 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -165,6 +165,5 @@ def device_state_attributes(self): name: value for name, value in ( (ATTR_LAST_TRIP, self._last_trip), (ATTR_FILENAME, self._filename), - (ATTR_WEBHOOK_URL, self.webhook_url), ) if value is not None } From 4e025a28d750a633a78ca7cab523657db076286e Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 9 Nov 2018 23:47:59 +0000 Subject: [PATCH 29/37] remove orphan attribute --- homeassistant/components/camera/push.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index 3c7a940f644811..0d2cc6c6276e95 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -31,7 +31,6 @@ ATTR_FILENAME = 'filename' ATTR_LAST_TRIP = 'last_trip' -ATTR_WEBHOOK_URL = 'post_camera_to' PUSH_CAMERA_DATA = 'push_camera' From 2b81c64693ef994e4beeec232ff8a7ff12302770 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 12:50:25 +0000 Subject: [PATCH 30/37] update tests --- tests/components/camera/test_push.py | 73 ++++++---------------------- 1 file changed, 14 insertions(+), 59 deletions(-) diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py index 6d9688c10e62ab..7d018a74c3e3c6 100644 --- a/tests/components/camera/test_push.py +++ b/tests/components/camera/test_push.py @@ -15,69 +15,24 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): 'camera': { 'platform': 'push', 'name': 'config_test', - 'token': '12345678' + 'webhook_id': 'camera.config_test' }}) client = await aiohttp_client(hass.http.app) - # missing file - resp = await client.post('/api/camera_push/camera.config_test') - assert resp.status == 400 - - # wrong entity + # wrong webhook files = {'image': io.BytesIO(b'fake')} resp = await client.post('/api/camera_push/camera.wrong', data=files) - assert resp.status == 404 - - -async def test_cases_with_no_auth(aioclient_mock, hass, aiohttp_client): - """Test cases where aiohttp_client is not auth.""" - await async_setup_component(hass, 'camera', { - 'camera': { - 'platform': 'push', - 'name': 'config_test', - 'token': '12345678' - }}) - - setup_auth(hass.http.app, [], True, api_password=None) - client = await aiohttp_client(hass.http.app) - - # wrong token - files = {'image': io.BytesIO(b'fake')} - resp = await client.post('/api/camera_push/camera.config_test?token=1234', - data=files) - assert resp.status == 401 - - # right token - files = {'image': io.BytesIO(b'fake')} - resp = await client.post( - '/api/camera_push/camera.config_test?token=12345678', - data=files) - assert resp.status == 200 - - -async def test_no_auth_no_token(aioclient_mock, hass, aiohttp_client): - """Test cases where aiohttp_client is not auth.""" - await async_setup_component(hass, 'camera', { - 'camera': { - 'platform': 'push', - 'name': 'config_test', - }}) - - setup_auth(hass.http.app, [], True, api_password=None) - client = await aiohttp_client(hass.http.app) - - # no token - files = {'image': io.BytesIO(b'fake')} - resp = await client.post('/api/camera_push/camera.config_test', - data=files) - assert resp.status == 401 + assert resp.status == 404 + + # missing file + camera_state = hass.states.get('camera.config_test') + assert camera_state.state == 'idle' + + resp = await client.post('/api/webhook/camera.config_test') + assert resp.status == 200 #webhooks always return 200 - # fake token - files = {'image': io.BytesIO(b'fake')} - resp = await client.post( - '/api/camera_push/camera.config_test?token=12345678', - data=files) - assert resp.status == 401 + camera_state = hass.states.get('camera.config_test') + assert camera_state.state == 'idle' #no file supplied we are still idle async def test_posting_url(hass, aiohttp_client): @@ -86,7 +41,7 @@ async def test_posting_url(hass, aiohttp_client): 'camera': { 'platform': 'push', 'name': 'config_test', - 'token': '12345678' + 'webhook_id': 'camera.config_test' }}) client = await aiohttp_client(hass.http.app) @@ -98,7 +53,7 @@ async def test_posting_url(hass, aiohttp_client): # post image resp = await client.post( - '/api/camera_push/camera.config_test?token=12345678', + '/api/webhook/camera.config_test', data=files) assert resp.status == 200 From ba66d563beaf4a45e5767cf84fc490929b40020b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 12:52:05 +0000 Subject: [PATCH 31/37] update tests --- tests/components/camera/test_push.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py index 7d018a74c3e3c6..405c38048d0787 100644 --- a/tests/components/camera/test_push.py +++ b/tests/components/camera/test_push.py @@ -6,7 +6,6 @@ from homeassistant import core as ha from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from homeassistant.components.http.auth import setup_auth async def test_bad_posting(aioclient_mock, hass, aiohttp_client): @@ -19,20 +18,20 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): }}) client = await aiohttp_client(hass.http.app) - # wrong webhook + # wrong webhook files = {'image': io.BytesIO(b'fake')} resp = await client.post('/api/camera_push/camera.wrong', data=files) - assert resp.status == 404 - + assert resp.status == 404 + # missing file camera_state = hass.states.get('camera.config_test') assert camera_state.state == 'idle' - + resp = await client.post('/api/webhook/camera.config_test') - assert resp.status == 200 #webhooks always return 200 + assert resp.status == 200 # webhooks always return 200 camera_state = hass.states.get('camera.config_test') - assert camera_state.state == 'idle' #no file supplied we are still idle + assert camera_state.state == 'idle' # no file supplied we are still idle async def test_posting_url(hass, aiohttp_client): From a7659345a11ad02ca2552545272febeb7bfbe28c Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 12:52:35 +0000 Subject: [PATCH 32/37] add warning --- homeassistant/components/camera/push.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index 0d2cc6c6276e95..617d0f9ba78b2a 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -73,6 +73,10 @@ async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook POST with image files.""" data = dict(await request.post()) camera = hass.data[PUSH_CAMERA_DATA][webhook_id] + + if camera.image_field not in data: + _LOGGER.warning("Webhook call without POST parameter <%s>", camera.image_field) + return await camera.update_image(data[camera.image_field].file.read(), data[camera.image_field].filename) From 196f1dce098ba7452acbc0ffdd6868fcd3e45928 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 12:53:47 +0000 Subject: [PATCH 33/37] hound fixes --- homeassistant/components/camera/push.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index 617d0f9ba78b2a..f2e03a3b946131 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -73,9 +73,10 @@ async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook POST with image files.""" data = dict(await request.post()) camera = hass.data[PUSH_CAMERA_DATA][webhook_id] - + if camera.image_field not in data: - _LOGGER.warning("Webhook call without POST parameter <%s>", camera.image_field) + _LOGGER.warning("Webhook call without POST parameter <%s>", + camera.image_field) return await camera.update_image(data[camera.image_field].file.read(), From bc5c53664aac8c971f3e0dc0a8d624ddc2d6d38c Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 14:13:03 +0000 Subject: [PATCH 34/37] block till done --- tests/components/camera/test_push.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py index 405c38048d0787..610e0cd8892a61 100644 --- a/tests/components/camera/test_push.py +++ b/tests/components/camera/test_push.py @@ -16,6 +16,7 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): 'name': 'config_test', 'webhook_id': 'camera.config_test' }}) + await hass.async_block_till_done() client = await aiohttp_client(hass.http.app) # wrong webhook @@ -42,6 +43,7 @@ async def test_posting_url(hass, aiohttp_client): 'name': 'config_test', 'webhook_id': 'camera.config_test' }}) + await hass.async_block_till_done() client = await aiohttp_client(hass.http.app) files = {'image': io.BytesIO(b'fake')} From b4d8c914e7afa6fa3754d2a637c096916401e044 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 15:08:57 +0000 Subject: [PATCH 35/37] trying to fix travis test --- tests/components/camera/test_push.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py index 610e0cd8892a61..0de328e228ed8a 100644 --- a/tests/components/camera/test_push.py +++ b/tests/components/camera/test_push.py @@ -4,12 +4,14 @@ from datetime import timedelta from homeassistant import core as ha +from homeassistant.components import webhook from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util async def test_bad_posting(aioclient_mock, hass, aiohttp_client): """Test that posting to wrong api endpoint fails.""" + await async_setup_component(hass, webhook.DOMAIN, {}) await async_setup_component(hass, 'camera', { 'camera': { 'platform': 'push', @@ -21,7 +23,7 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): # wrong webhook files = {'image': io.BytesIO(b'fake')} - resp = await client.post('/api/camera_push/camera.wrong', data=files) + resp = await client.post('/api/webhood/camera.wrong', data=files) assert resp.status == 404 # missing file @@ -37,6 +39,7 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): async def test_posting_url(hass, aiohttp_client): """Test that posting to api endpoint works.""" + await async_setup_component(hass, webhook.DOMAIN, {}) await async_setup_component(hass, 'camera', { 'camera': { 'platform': 'push', From 4c81a3d0a91b78bd4ed11ca1b49e15799e69e054 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 15:13:47 +0000 Subject: [PATCH 36/37] trying to fix travis test --- tests/components/camera/test_push.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py index 0de328e228ed8a..be1d24ce34fbb4 100644 --- a/tests/components/camera/test_push.py +++ b/tests/components/camera/test_push.py @@ -19,6 +19,8 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client): 'webhook_id': 'camera.config_test' }}) await hass.async_block_till_done() + assert hass.states.get('camera.config_test') is not None + client = await aiohttp_client(hass.http.app) # wrong webhook From 8b34bd173f4ef765df467547c2b1b67a22539eff Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 11 Nov 2018 17:08:06 +0000 Subject: [PATCH 37/37] finally realised there was 8206 --- homeassistant/components/camera/push.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index f2e03a3b946131..ad8aef8e287cb7 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\ - STATE_IDLE, STATE_RECORDING + STATE_IDLE, STATE_RECORDING, DOMAIN from homeassistant.core import callback from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID from homeassistant.helpers import config_validation as cv @@ -60,7 +60,7 @@ async def async_setup_platform(hass, config, async_add_entities, webhook_id)] try: - hass.components.webhook.async_register(webhook_id, handle_webhook) + hass.components.webhook.async_register(DOMAIN, 'push', webhook_id, handle_webhook) except ValueError: _LOGGER.error("In <%s>, webhook_id <%s> already used", config[CONF_NAME], webhook_id)