mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
Merge pull request #59555 from home-assistant/rc
This commit is contained in:
commit
6f3f16dbc9
@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20211108.0"
|
||||
"home-assistant-frontend==20211109.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
@ -156,6 +156,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
|
||||
if (
|
||||
old_state is None
|
||||
or new_state is None
|
||||
or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
):
|
||||
|
@ -119,10 +119,13 @@ class RaspberryCamera(Camera):
|
||||
cmd_args.append("-a")
|
||||
cmd_args.append(str(device_info[CONF_OVERLAY_TIMESTAMP]))
|
||||
|
||||
with subprocess.Popen(
|
||||
# The raspistill process started below must run "forever" in
|
||||
# the background until killed when Home Assistant is stopped.
|
||||
# Therefore it must not be wrapped with "with", since that
|
||||
# waits for the subprocess to exit before continuing.
|
||||
subprocess.Popen( # pylint: disable=consider-using-with
|
||||
cmd_args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
|
||||
):
|
||||
pass
|
||||
)
|
||||
|
||||
def camera_image(
|
||||
self, width: int | None = None, height: int | None = None
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Support for monitoring a Sense energy sensor."""
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
STATE_CLASS_TOTAL,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
@ -251,7 +251,7 @@ class SenseTrendsSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Implementation of a Sense energy sensor."""
|
||||
|
||||
_attr_device_class = DEVICE_CLASS_ENERGY
|
||||
_attr_state_class = STATE_CLASS_TOTAL_INCREASING
|
||||
_attr_state_class = STATE_CLASS_TOTAL
|
||||
_attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR
|
||||
_attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
_attr_icon = ICON
|
||||
|
@ -30,7 +30,7 @@ class SonosDiscoveryFlowHandler(DiscoveryFlowHandler):
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by zeroconf."""
|
||||
hostname = discovery_info["hostname"]
|
||||
if hostname is None or not hostname.startswith("Sonos-"):
|
||||
if hostname is None or not hostname.lower().startswith("sonos"):
|
||||
return self.async_abort(reason="not_sonos_device")
|
||||
await self.async_set_unique_id(self._domain, raise_on_progress=False)
|
||||
host = discovery_info[CONF_HOST]
|
||||
|
@ -44,5 +44,10 @@ def soco_error(errorcodes: list[str] | None = None) -> Callable:
|
||||
|
||||
def hostname_to_uid(hostname: str) -> str:
|
||||
"""Convert a Sonos hostname to a uid."""
|
||||
if hostname.startswith("Sonos-"):
|
||||
baseuid = hostname.split("-")[1].replace(".local.", "")
|
||||
elif hostname.startswith("sonos"):
|
||||
baseuid = hostname[5:].replace(".local.", "")
|
||||
else:
|
||||
raise ValueError(f"{hostname} is not a sonos device.")
|
||||
return f"{UID_PREFIX}{baseuid}{UID_POSTFIX}"
|
||||
|
@ -77,6 +77,16 @@ class HlsStreamOutput(StreamOutput):
|
||||
or self.stream_settings.min_segment_duration
|
||||
)
|
||||
|
||||
def discontinuity(self) -> None:
|
||||
"""Remove incomplete segment from deque."""
|
||||
self._hass.loop.call_soon_threadsafe(self._async_discontinuity)
|
||||
|
||||
@callback
|
||||
def _async_discontinuity(self) -> None:
|
||||
"""Remove incomplete segment from deque in event loop."""
|
||||
if self._segments and not self._segments[-1].complete:
|
||||
self._segments.pop()
|
||||
|
||||
|
||||
class HlsMasterPlaylistView(StreamView):
|
||||
"""Stream view used only for Chromecast compatibility."""
|
||||
|
@ -18,6 +18,7 @@ from .const import (
|
||||
ATTR_SETTINGS,
|
||||
AUDIO_CODECS,
|
||||
DOMAIN,
|
||||
HLS_PROVIDER,
|
||||
MAX_MISSING_DTS,
|
||||
MAX_TIMESTAMP_GAP,
|
||||
PACKETS_TO_WAIT_FOR_AUDIO,
|
||||
@ -25,6 +26,7 @@ from .const import (
|
||||
SOURCE_TIMEOUT,
|
||||
)
|
||||
from .core import Part, Segment, StreamOutput, StreamSettings
|
||||
from .hls import HlsStreamOutput
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -279,6 +281,9 @@ class SegmentBuffer:
|
||||
# the discontinuity sequence number.
|
||||
self._stream_id += 1
|
||||
self._start_time = datetime.datetime.utcnow()
|
||||
# Call discontinuity to remove incomplete segment from the HLS output
|
||||
if hls_output := self._outputs_callback().get(HLS_PROVIDER):
|
||||
cast(HlsStreamOutput, hls_output).discontinuity()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close stream buffer."""
|
||||
|
@ -73,16 +73,14 @@ class WirelessTagPlatform:
|
||||
|
||||
def arm(self, switch):
|
||||
"""Arm entity sensor monitoring."""
|
||||
func_name = f"arm_{switch.sensor_type}"
|
||||
arm_func = getattr(self.api, func_name)
|
||||
if arm_func is not None:
|
||||
func_name = f"arm_{switch.entity_description.key}"
|
||||
if (arm_func := getattr(self.api, func_name)) is not None:
|
||||
arm_func(switch.tag_id, switch.tag_manager_mac)
|
||||
|
||||
def disarm(self, switch):
|
||||
"""Disarm entity sensor monitoring."""
|
||||
func_name = f"disarm_{switch.sensor_type}"
|
||||
disarm_func = getattr(self.api, func_name)
|
||||
if disarm_func is not None:
|
||||
func_name = f"disarm_{switch.entity_description.key}"
|
||||
if (disarm_func := getattr(self.api, func_name)) is not None:
|
||||
disarm_func(switch.tag_id, switch.tag_manager_mac)
|
||||
|
||||
def start_monitoring(self):
|
||||
|
@ -5,7 +5,7 @@ from typing import Final
|
||||
|
||||
MAJOR_VERSION: Final = 2021
|
||||
MINOR_VERSION: Final = 11
|
||||
PATCH_VERSION: Final = "2"
|
||||
PATCH_VERSION: Final = "3"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||
|
@ -15,7 +15,7 @@ ciso8601==2.2.0
|
||||
cryptography==3.4.8
|
||||
emoji==1.5.0
|
||||
hass-nabucasa==0.50.0
|
||||
home-assistant-frontend==20211108.0
|
||||
home-assistant-frontend==20211109.0
|
||||
httpx==0.19.0
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.0.2
|
||||
|
@ -813,7 +813,7 @@ hole==0.5.1
|
||||
holidays==0.11.3.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20211108.0
|
||||
home-assistant-frontend==20211109.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
|
@ -500,7 +500,7 @@ hole==0.5.1
|
||||
holidays==0.11.3.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20211108.0
|
||||
home-assistant-frontend==20211109.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
|
@ -75,6 +75,56 @@ async def test_zeroconf_form(hass: core.HomeAssistant):
|
||||
assert len(mock_manager.mock_calls) == 2
|
||||
|
||||
|
||||
async def test_zeroconf_sonos_v1(hass: core.HomeAssistant):
|
||||
"""Test we pass sonos devices to the discovery manager with v1 firmware devices."""
|
||||
|
||||
mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data={
|
||||
"host": "192.168.1.107",
|
||||
"port": 1443,
|
||||
"hostname": "sonos5CAAFDE47AC8.local.",
|
||||
"type": "_sonos._tcp.local.",
|
||||
"name": "Sonos-5CAAFDE47AC8._sonos._tcp.local.",
|
||||
"properties": {
|
||||
"_raw": {
|
||||
"info": b"/api/v1/players/RINCON_5CAAFDE47AC801400/info",
|
||||
"vers": b"1",
|
||||
"protovers": b"1.18.9",
|
||||
},
|
||||
"info": "/api/v1/players/RINCON_5CAAFDE47AC801400/info",
|
||||
"vers": "1",
|
||||
"protovers": "1.18.9",
|
||||
},
|
||||
},
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.sonos.async_setup",
|
||||
return_value=True,
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.sonos.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "Sonos"
|
||||
assert result2["data"] == {}
|
||||
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
assert len(mock_manager.mock_calls) == 2
|
||||
|
||||
|
||||
async def test_zeroconf_form_not_sonos(hass: core.HomeAssistant):
|
||||
"""Test we abort on non-sonos devices."""
|
||||
mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock()
|
||||
|
@ -1,9 +1,15 @@
|
||||
"""Test the sonos config flow."""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sonos.helpers import hostname_to_uid
|
||||
|
||||
|
||||
async def test_uid_to_hostname():
|
||||
"""Test we can convert a hostname to a uid."""
|
||||
assert hostname_to_uid("Sonos-347E5C0CF1E3.local.") == "RINCON_347E5C0CF1E301400"
|
||||
assert hostname_to_uid("sonos5CAAFDE47AC8.local.") == "RINCON_5CAAFDE47AC801400"
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert hostname_to_uid("notsonos5CAAFDE47AC8.local.")
|
||||
|
@ -22,8 +22,8 @@ from aiohttp import web
|
||||
import async_timeout
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.stream import Stream
|
||||
from homeassistant.components.stream.core import Segment, StreamOutput
|
||||
from homeassistant.components.stream.worker import SegmentBuffer
|
||||
|
||||
TEST_TIMEOUT = 7.0 # Lower than 9s home assistant timeout
|
||||
|
||||
@ -34,7 +34,7 @@ class WorkerSync:
|
||||
def __init__(self):
|
||||
"""Initialize WorkerSync."""
|
||||
self._event = None
|
||||
self._original = Stream._worker_finished
|
||||
self._original = SegmentBuffer.discontinuity
|
||||
|
||||
def pause(self):
|
||||
"""Pause the worker before it finalizes the stream."""
|
||||
@ -45,7 +45,7 @@ class WorkerSync:
|
||||
logging.debug("waking blocked worker")
|
||||
self._event.set()
|
||||
|
||||
def blocking_finish(self, stream: Stream):
|
||||
def blocking_discontinuity(self, stream: SegmentBuffer):
|
||||
"""Intercept call to pause stream worker."""
|
||||
# Worker is ending the stream, which clears all output buffers.
|
||||
# Block the worker thread until the test has a chance to verify
|
||||
@ -63,8 +63,8 @@ def stream_worker_sync(hass):
|
||||
"""Patch StreamOutput to allow test to synchronize worker stream end."""
|
||||
sync = WorkerSync()
|
||||
with patch(
|
||||
"homeassistant.components.stream.Stream._worker_finished",
|
||||
side_effect=sync.blocking_finish,
|
||||
"homeassistant.components.stream.worker.SegmentBuffer.discontinuity",
|
||||
side_effect=sync.blocking_discontinuity,
|
||||
autospec=True,
|
||||
):
|
||||
yield sync
|
||||
|
@ -448,3 +448,33 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy
|
||||
|
||||
stream_worker_sync.resume()
|
||||
stream.stop()
|
||||
|
||||
|
||||
async def test_remove_incomplete_segment_on_exit(hass, stream_worker_sync):
|
||||
"""Test that the incomplete segment gets removed when the worker thread quits."""
|
||||
await async_setup_component(hass, "stream", {"stream": {}})
|
||||
|
||||
stream = create_stream(hass, STREAM_SOURCE, {})
|
||||
stream_worker_sync.pause()
|
||||
stream.start()
|
||||
hls = stream.add_provider(HLS_PROVIDER)
|
||||
|
||||
segment = Segment(sequence=0, stream_id=0, duration=SEGMENT_DURATION)
|
||||
hls.put(segment)
|
||||
segment = Segment(sequence=1, stream_id=0, duration=SEGMENT_DURATION)
|
||||
hls.put(segment)
|
||||
segment = Segment(sequence=2, stream_id=0, duration=0)
|
||||
hls.put(segment)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
segments = hls._segments
|
||||
assert len(segments) == 3
|
||||
assert not segments[-1].complete
|
||||
stream_worker_sync.resume()
|
||||
stream._thread_quit.set()
|
||||
stream._thread.join()
|
||||
stream._thread = None
|
||||
await hass.async_block_till_done()
|
||||
assert segments[-1].complete
|
||||
assert len(segments) == 2
|
||||
stream.stop()
|
||||
|
Loading…
x
Reference in New Issue
Block a user