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() 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( 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/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 2a12b59e7c7..448c66c4e45 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]) @@ -799,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) @@ -893,16 +900,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): 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) diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 24d74afa6da..0c69e453092 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -259,3 +259,30 @@ 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, + ) + 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': [] + } + }