Merge pull request #13369 from home-assistant/release-0-65-6

0.65.6
This commit is contained in:
Paulus Schoutsen 2018-03-21 10:59:58 -07:00 committed by GitHub
commit c3974e540b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 72 additions and 45 deletions

View File

@ -56,34 +56,6 @@ async def async_setup_platform(hass, config, async_add_devices,
async_add_devices([ProxyCamera(hass, config)]) 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): def _resize_image(image, opts):
"""Resize image.""" """Resize image."""
from PIL import Image from PIL import Image
@ -227,9 +199,9 @@ class ProxyCamera(Camera):
'boundary=--frameboundary') 'boundary=--frameboundary')
await response.prepare(request) await response.prepare(request)
def write(img_bytes): async def write(img_bytes):
"""Write image to stream.""" """Write image to stream."""
response.write(bytes( await response.write(bytes(
'--frameboundary\r\n' '--frameboundary\r\n'
'Content-Type: {}\r\n' 'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format( 'Content-Length: {}\r\n\r\n'.format(
@ -240,13 +212,23 @@ class ProxyCamera(Camera):
req = await stream_coro req = await stream_coro
try: 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: while True:
image = await _read_frame(req) chunk = await stream.read(102400)
if not image: if not chunk:
break break
image = await self.hass.async_add_job( data += chunk
_resize_image, image, self._stream_opts) jpg_start = data.find(b'\xff\xd8')
write(image) 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: except asyncio.CancelledError:
_LOGGER.debug("Stream closed by frontend.") _LOGGER.debug("Stream closed by frontend.")
req.close() req.close()

View File

@ -100,7 +100,7 @@ class TadoDeviceScanner(DeviceScanner):
last_results = [] last_results = []
try: 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 # Format the URL here, so we can log the template URL if
# anything goes wrong without exposing username and password. # anything goes wrong without exposing username and password.
url = self.tadoapiurl.format( url = self.tadoapiurl.format(

View File

@ -94,9 +94,16 @@ class _GoogleEntity:
https://developers.google.com/actions/smarthome/create-app#actiondevicessync https://developers.google.com/actions/smarthome/create-app#actiondevicessync
""" """
traits = self.traits()
state = self.state 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 # Found no supported traits for this entity
if not traits: if not traits:
return None return None

View File

@ -54,6 +54,7 @@ class DemoLight(Light):
self._white = white self._white = white
self._effect_list = effect_list self._effect_list = effect_list
self._effect = effect self._effect = effect
self._available = True
@property @property
def should_poll(self) -> bool: def should_poll(self) -> bool:
@ -75,7 +76,7 @@ class DemoLight(Light):
"""Return availability.""" """Return availability."""
# This demo light is always available, but well-behaving components # This demo light is always available, but well-behaving components
# should implement this to inform Home Assistant accordingly. # should implement this to inform Home Assistant accordingly.
return True return self._available
@property @property
def brightness(self) -> int: def brightness(self) -> int:

View File

@ -194,13 +194,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
master = [device for device in hass.data[DATA_SONOS].devices master = [device for device in hass.data[DATA_SONOS].devices
if device.entity_id == service.data[ATTR_MASTER]] if device.entity_id == service.data[ATTR_MASTER]]
if 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 return
for device in devices: for device in devices:
if service.service == SERVICE_UNJOIN: if service.service == SERVICE_SNAPSHOT:
device.unjoin()
elif service.service == SERVICE_SNAPSHOT:
device.snapshot(service.data[ATTR_WITH_GROUP]) device.snapshot(service.data[ATTR_WITH_GROUP])
elif service.service == SERVICE_RESTORE: elif service.service == SERVICE_RESTORE:
device.restore(service.data[ATTR_WITH_GROUP]) device.restore(service.data[ATTR_WITH_GROUP])
@ -799,7 +804,9 @@ class SonosDevice(MediaPlayerDevice):
src = fav.pop() src = fav.pop()
uri = src.reference.get_uri() uri = src.reference.get_uri()
if _is_radio_uri(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: else:
self.soco.clear_queue() self.soco.clear_queue()
self.soco.add_to_queue(src.reference) self.soco.add_to_queue(src.reference)
@ -893,16 +900,19 @@ class SonosDevice(MediaPlayerDevice):
def join(self, slaves): def join(self, slaves):
"""Form a group with other players.""" """Form a group with other players."""
if self._coordinator: if self._coordinator:
self.soco.unjoin() self.unjoin()
for slave in slaves: for slave in slaves:
if slave.unique_id != self.unique_id: if slave.unique_id != self.unique_id:
slave.soco.join(self.soco) slave.soco.join(self.soco)
# pylint: disable=protected-access
slave._coordinator = self
@soco_error() @soco_error()
def unjoin(self): def unjoin(self):
"""Unjoin the player from a group.""" """Unjoin the player from a group."""
self.soco.unjoin() self.soco.unjoin()
self._coordinator = None
@soco_error() @soco_error()
def snapshot(self, with_group=True): def snapshot(self, with_group=True):

View File

@ -2,7 +2,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 65 MINOR_VERSION = 65
PATCH_VERSION = '5' PATCH_VERSION = '6'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3) REQUIRED_PYTHON_VER = (3, 5, 3)

View File

@ -259,3 +259,30 @@ def test_serialize_input_boolean():
'type': 'action.devices.types.SWITCH', 'type': 'action.devices.types.SWITCH',
'willReportState': False, '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': []
}
}