commit
c3974e540b
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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': []
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue