mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
commit
c3974e540b
@ -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()
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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': []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user