From fab958d789cf7f5727451722af95b2dc0fd7855d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Mar 2018 10:14:28 -0700 Subject: [PATCH 1/7] Version bump to 0.65.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7e2b9f3061a..7fe26e6c334 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 65 -PATCH_VERSION = '5' +PATCH_VERSION = '6' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 2388d62755252ac4e35159880fe4b6c17c954789 Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Wed, 14 Mar 2018 21:44:13 -0700 Subject: [PATCH 2/7] More robust MJPEG parser. Fixes #13138. (#13226) * More robust MJPEG parser. Fixes ##13138. * Reimplement image extraction from mjpeg without ascy generator to support python 3.5 --- homeassistant/components/camera/proxy.py | 52 ++++++++---------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index 56b9db5c0ec..9f261b89bb2 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -56,34 +56,6 @@ async def async_setup_platform(hass, config, async_add_devices, async_add_devices([ProxyCamera(hass, config)]) -async def _read_frame(req): - """Read a single frame from an MJPEG stream.""" - # based on https://gist.github.com/russss/1143799 - import cgi - # Read in HTTP headers: - stream = req.content - # multipart/x-mixed-replace; boundary=--frameboundary - _mimetype, options = cgi.parse_header(req.headers['content-type']) - boundary = options.get('boundary').encode('utf-8') - if not boundary: - _LOGGER.error("Malformed MJPEG missing boundary") - raise Exception("Can't find content-type") - - line = await stream.readline() - # Seek ahead to the first chunk - while line.strip() != boundary: - line = await stream.readline() - # Read in chunk headers - while line.strip() != b'': - parts = line.split(b':') - if len(parts) > 1 and parts[0].lower() == b'content-length': - # Grab chunk length - length = int(parts[1].strip()) - line = await stream.readline() - image = await stream.read(length) - return image - - def _resize_image(image, opts): """Resize image.""" from PIL import Image @@ -227,9 +199,9 @@ class ProxyCamera(Camera): 'boundary=--frameboundary') await response.prepare(request) - def write(img_bytes): + async def write(img_bytes): """Write image to stream.""" - response.write(bytes( + await response.write(bytes( '--frameboundary\r\n' 'Content-Type: {}\r\n' 'Content-Length: {}\r\n\r\n'.format( @@ -240,13 +212,23 @@ class ProxyCamera(Camera): req = await stream_coro try: + # This would be nicer as an async generator + # But that would only be supported for python >=3.6 + data = b'' + stream = req.content while True: - image = await _read_frame(req) - if not image: + chunk = await stream.read(102400) + if not chunk: break - image = await self.hass.async_add_job( - _resize_image, image, self._stream_opts) - write(image) + data += chunk + jpg_start = data.find(b'\xff\xd8') + jpg_end = data.find(b'\xff\xd9') + if jpg_start != -1 and jpg_end != -1: + image = data[jpg_start:jpg_end + 2] + image = await self.hass.async_add_job( + _resize_image, image, self._stream_opts) + await write(image) + data = data[jpg_end + 2:] except asyncio.CancelledError: _LOGGER.debug("Stream closed by frontend.") req.close() From 7e08e8bd51930100d3ec621b007167de6005c93d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Mar 2018 13:53:59 -0700 Subject: [PATCH 3/7] Tado: don't reference unset hass var (#13237) Tado: don't reference unset hass var --- homeassistant/components/device_tracker/tado.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/tado.py b/homeassistant/components/device_tracker/tado.py index 11d12322ff5..ef816338ce9 100644 --- a/homeassistant/components/device_tracker/tado.py +++ b/homeassistant/components/device_tracker/tado.py @@ -100,7 +100,7 @@ class TadoDeviceScanner(DeviceScanner): last_results = [] try: - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): # Format the URL here, so we can log the template URL if # anything goes wrong without exposing username and password. url = self.tadoapiurl.format( From 0de26817833c4f430e4a94045fca489e6ceac64e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 16 Mar 2018 00:12:43 +0100 Subject: [PATCH 4/7] Fix Sonos join/unjoin in scripts (#13248) --- homeassistant/components/media_player/sonos.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 2a12b59e7c7..b2cbffed891 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -194,13 +194,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): master = [device for device in hass.data[DATA_SONOS].devices if device.entity_id == service.data[ATTR_MASTER]] if master: - master[0].join(devices) + with hass.data[DATA_SONOS].topology_lock: + master[0].join(devices) + return + + if service.service == SERVICE_UNJOIN: + with hass.data[DATA_SONOS].topology_lock: + for device in devices: + device.unjoin() return for device in devices: - if service.service == SERVICE_UNJOIN: - device.unjoin() - elif service.service == SERVICE_SNAPSHOT: + if service.service == SERVICE_SNAPSHOT: device.snapshot(service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_RESTORE: device.restore(service.data[ATTR_WITH_GROUP]) @@ -893,16 +898,19 @@ class SonosDevice(MediaPlayerDevice): def join(self, slaves): """Form a group with other players.""" if self._coordinator: - self.soco.unjoin() + self.unjoin() for slave in slaves: if slave.unique_id != self.unique_id: slave.soco.join(self.soco) + # pylint: disable=protected-access + slave._coordinator = self @soco_error() def unjoin(self): """Unjoin the player from a group.""" self.soco.unjoin() + self._coordinator = None @soco_error() def snapshot(self, with_group=True): From 7718f70c5f54b32821f4a52fa53054073b20a354 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 21 Mar 2018 02:05:03 +0100 Subject: [PATCH 5/7] Fix Sonos radio stations with ampersand (#13293) --- homeassistant/components/media_player/sonos.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index b2cbffed891..448c66c4e45 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -804,7 +804,9 @@ class SonosDevice(MediaPlayerDevice): src = fav.pop() uri = src.reference.get_uri() if _is_radio_uri(uri): - self.soco.play_uri(uri, title=source) + # SoCo 0.14 fails to XML escape the title parameter + from xml.sax.saxutils import escape + self.soco.play_uri(uri, title=escape(source)) else: self.soco.clear_queue() self.soco.add_to_queue(src.reference) From ffbafa687ab25fd3563841c104a5a160a3f8347f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Mar 2018 18:09:34 -0700 Subject: [PATCH 6/7] Do not include unavailable entities in Google Assistant SYNC (#13358) --- .../components/google_assistant/smart_home.py | 9 +++++- homeassistant/components/light/demo.py | 3 +- .../google_assistant/test_smart_home.py | 28 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 834d40c367c..7e746d48bed 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -94,9 +94,16 @@ class _GoogleEntity: https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ - traits = self.traits() state = self.state + # When a state is unavailable, the attributes that describe + # capabilities will be stripped. For example, a light entity will miss + # the min/max mireds. Therefore they will be excluded from a sync. + if state.state == STATE_UNAVAILABLE: + return None + + traits = self.traits() + # Found no supported traits for this entity if not traits: return None diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index acc70a57ff4..37a354bb3f2 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -54,6 +54,7 @@ class DemoLight(Light): self._white = white self._effect_list = effect_list self._effect = effect + self._available = True @property def should_poll(self) -> bool: @@ -75,7 +76,7 @@ class DemoLight(Light): """Return availability.""" # This demo light is always available, but well-behaving components # should implement this to inform Home Assistant accordingly. - return True + return self._available @property def brightness(self) -> int: diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 24d74afa6da..8c9824a32f8 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -259,3 +259,31 @@ def test_serialize_input_boolean(): 'type': 'action.devices.types.SWITCH', 'willReportState': False, } + + +async def test_unavailable_state_doesnt_sync(hass): + """Test that an unavailable entity does not sync over.""" + light = DemoLight( + None, 'Demo Light', + state=False, + hs_color=(180, 75), + ) + light.hass = hass + light.entity_id = 'light.demo_light' + light._available = False + await light.async_update_ha_state() + + result = await sh.async_handle_message(hass, BASIC_CONFIG, { + "requestId": REQ_ID, + "inputs": [{ + "intent": "action.devices.SYNC" + }] + }) + + assert result == { + 'requestId': REQ_ID, + 'payload': { + 'agentUserId': 'test-agent', + 'devices': [] + } + } From da4e630f5476de38ab422597e8b1941b26678969 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Mar 2018 10:38:06 -0700 Subject: [PATCH 7/7] Fix tests --- tests/components/google_assistant/test_smart_home.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 8c9824a32f8..0c69e453092 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -266,7 +266,6 @@ async def test_unavailable_state_doesnt_sync(hass): light = DemoLight( None, 'Demo Light', state=False, - hs_color=(180, 75), ) light.hass = hass light.entity_id = 'light.demo_light'