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",
|
"name": "Home Assistant Frontend",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"home-assistant-frontend==20211108.0"
|
"home-assistant-frontend==20211109.0"
|
||||||
],
|
],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"api",
|
"api",
|
||||||
|
@ -156,6 +156,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
old_state is None
|
old_state is None
|
||||||
|
or new_state is None
|
||||||
or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||||
or new_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("-a")
|
||||||
cmd_args.append(str(device_info[CONF_OVERLAY_TIMESTAMP]))
|
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
|
cmd_args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
|
||||||
):
|
)
|
||||||
pass
|
|
||||||
|
|
||||||
def camera_image(
|
def camera_image(
|
||||||
self, width: int | None = None, height: int | None = None
|
self, width: int | None = None, height: int | None = None
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Support for monitoring a Sense energy sensor."""
|
"""Support for monitoring a Sense energy sensor."""
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
STATE_CLASS_TOTAL_INCREASING,
|
STATE_CLASS_TOTAL,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -251,7 +251,7 @@ class SenseTrendsSensor(CoordinatorEntity, SensorEntity):
|
|||||||
"""Implementation of a Sense energy sensor."""
|
"""Implementation of a Sense energy sensor."""
|
||||||
|
|
||||||
_attr_device_class = DEVICE_CLASS_ENERGY
|
_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_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR
|
||||||
_attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
_attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||||
_attr_icon = ICON
|
_attr_icon = ICON
|
||||||
|
@ -30,7 +30,7 @@ class SonosDiscoveryFlowHandler(DiscoveryFlowHandler):
|
|||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
"""Handle a flow initialized by zeroconf."""
|
"""Handle a flow initialized by zeroconf."""
|
||||||
hostname = discovery_info["hostname"]
|
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")
|
return self.async_abort(reason="not_sonos_device")
|
||||||
await self.async_set_unique_id(self._domain, raise_on_progress=False)
|
await self.async_set_unique_id(self._domain, raise_on_progress=False)
|
||||||
host = discovery_info[CONF_HOST]
|
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:
|
def hostname_to_uid(hostname: str) -> str:
|
||||||
"""Convert a Sonos hostname to a uid."""
|
"""Convert a Sonos hostname to a uid."""
|
||||||
baseuid = hostname.split("-")[1].replace(".local.", "")
|
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}"
|
return f"{UID_PREFIX}{baseuid}{UID_POSTFIX}"
|
||||||
|
@ -77,6 +77,16 @@ class HlsStreamOutput(StreamOutput):
|
|||||||
or self.stream_settings.min_segment_duration
|
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):
|
class HlsMasterPlaylistView(StreamView):
|
||||||
"""Stream view used only for Chromecast compatibility."""
|
"""Stream view used only for Chromecast compatibility."""
|
||||||
|
@ -18,6 +18,7 @@ from .const import (
|
|||||||
ATTR_SETTINGS,
|
ATTR_SETTINGS,
|
||||||
AUDIO_CODECS,
|
AUDIO_CODECS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
HLS_PROVIDER,
|
||||||
MAX_MISSING_DTS,
|
MAX_MISSING_DTS,
|
||||||
MAX_TIMESTAMP_GAP,
|
MAX_TIMESTAMP_GAP,
|
||||||
PACKETS_TO_WAIT_FOR_AUDIO,
|
PACKETS_TO_WAIT_FOR_AUDIO,
|
||||||
@ -25,6 +26,7 @@ from .const import (
|
|||||||
SOURCE_TIMEOUT,
|
SOURCE_TIMEOUT,
|
||||||
)
|
)
|
||||||
from .core import Part, Segment, StreamOutput, StreamSettings
|
from .core import Part, Segment, StreamOutput, StreamSettings
|
||||||
|
from .hls import HlsStreamOutput
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -279,6 +281,9 @@ class SegmentBuffer:
|
|||||||
# the discontinuity sequence number.
|
# the discontinuity sequence number.
|
||||||
self._stream_id += 1
|
self._stream_id += 1
|
||||||
self._start_time = datetime.datetime.utcnow()
|
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:
|
def close(self) -> None:
|
||||||
"""Close stream buffer."""
|
"""Close stream buffer."""
|
||||||
|
@ -73,16 +73,14 @@ class WirelessTagPlatform:
|
|||||||
|
|
||||||
def arm(self, switch):
|
def arm(self, switch):
|
||||||
"""Arm entity sensor monitoring."""
|
"""Arm entity sensor monitoring."""
|
||||||
func_name = f"arm_{switch.sensor_type}"
|
func_name = f"arm_{switch.entity_description.key}"
|
||||||
arm_func = getattr(self.api, func_name)
|
if (arm_func := getattr(self.api, func_name)) is not None:
|
||||||
if arm_func is not None:
|
|
||||||
arm_func(switch.tag_id, switch.tag_manager_mac)
|
arm_func(switch.tag_id, switch.tag_manager_mac)
|
||||||
|
|
||||||
def disarm(self, switch):
|
def disarm(self, switch):
|
||||||
"""Disarm entity sensor monitoring."""
|
"""Disarm entity sensor monitoring."""
|
||||||
func_name = f"disarm_{switch.sensor_type}"
|
func_name = f"disarm_{switch.entity_description.key}"
|
||||||
disarm_func = getattr(self.api, func_name)
|
if (disarm_func := getattr(self.api, func_name)) is not None:
|
||||||
if disarm_func is not None:
|
|
||||||
disarm_func(switch.tag_id, switch.tag_manager_mac)
|
disarm_func(switch.tag_id, switch.tag_manager_mac)
|
||||||
|
|
||||||
def start_monitoring(self):
|
def start_monitoring(self):
|
||||||
|
@ -5,7 +5,7 @@ from typing import Final
|
|||||||
|
|
||||||
MAJOR_VERSION: Final = 2021
|
MAJOR_VERSION: Final = 2021
|
||||||
MINOR_VERSION: Final = 11
|
MINOR_VERSION: Final = 11
|
||||||
PATCH_VERSION: Final = "2"
|
PATCH_VERSION: Final = "3"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||||
|
@ -15,7 +15,7 @@ ciso8601==2.2.0
|
|||||||
cryptography==3.4.8
|
cryptography==3.4.8
|
||||||
emoji==1.5.0
|
emoji==1.5.0
|
||||||
hass-nabucasa==0.50.0
|
hass-nabucasa==0.50.0
|
||||||
home-assistant-frontend==20211108.0
|
home-assistant-frontend==20211109.0
|
||||||
httpx==0.19.0
|
httpx==0.19.0
|
||||||
ifaddr==0.1.7
|
ifaddr==0.1.7
|
||||||
jinja2==3.0.2
|
jinja2==3.0.2
|
||||||
|
@ -813,7 +813,7 @@ hole==0.5.1
|
|||||||
holidays==0.11.3.1
|
holidays==0.11.3.1
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20211108.0
|
home-assistant-frontend==20211109.0
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.10
|
homeassistant-pyozw==0.1.10
|
||||||
|
@ -500,7 +500,7 @@ hole==0.5.1
|
|||||||
holidays==0.11.3.1
|
holidays==0.11.3.1
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20211108.0
|
home-assistant-frontend==20211109.0
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.10
|
homeassistant-pyozw==0.1.10
|
||||||
|
@ -75,6 +75,56 @@ async def test_zeroconf_form(hass: core.HomeAssistant):
|
|||||||
assert len(mock_manager.mock_calls) == 2
|
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):
|
async def test_zeroconf_form_not_sonos(hass: core.HomeAssistant):
|
||||||
"""Test we abort on non-sonos devices."""
|
"""Test we abort on non-sonos devices."""
|
||||||
mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock()
|
mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock()
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
"""Test the sonos config flow."""
|
"""Test the sonos config flow."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.sonos.helpers import hostname_to_uid
|
from homeassistant.components.sonos.helpers import hostname_to_uid
|
||||||
|
|
||||||
|
|
||||||
async def test_uid_to_hostname():
|
async def test_uid_to_hostname():
|
||||||
"""Test we can convert a hostname to a uid."""
|
"""Test we can convert a hostname to a uid."""
|
||||||
assert hostname_to_uid("Sonos-347E5C0CF1E3.local.") == "RINCON_347E5C0CF1E301400"
|
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 async_timeout
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.stream import Stream
|
|
||||||
from homeassistant.components.stream.core import Segment, StreamOutput
|
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
|
TEST_TIMEOUT = 7.0 # Lower than 9s home assistant timeout
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ class WorkerSync:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize WorkerSync."""
|
"""Initialize WorkerSync."""
|
||||||
self._event = None
|
self._event = None
|
||||||
self._original = Stream._worker_finished
|
self._original = SegmentBuffer.discontinuity
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
"""Pause the worker before it finalizes the stream."""
|
"""Pause the worker before it finalizes the stream."""
|
||||||
@ -45,7 +45,7 @@ class WorkerSync:
|
|||||||
logging.debug("waking blocked worker")
|
logging.debug("waking blocked worker")
|
||||||
self._event.set()
|
self._event.set()
|
||||||
|
|
||||||
def blocking_finish(self, stream: Stream):
|
def blocking_discontinuity(self, stream: SegmentBuffer):
|
||||||
"""Intercept call to pause stream worker."""
|
"""Intercept call to pause stream worker."""
|
||||||
# Worker is ending the stream, which clears all output buffers.
|
# Worker is ending the stream, which clears all output buffers.
|
||||||
# Block the worker thread until the test has a chance to verify
|
# 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."""
|
"""Patch StreamOutput to allow test to synchronize worker stream end."""
|
||||||
sync = WorkerSync()
|
sync = WorkerSync()
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.stream.Stream._worker_finished",
|
"homeassistant.components.stream.worker.SegmentBuffer.discontinuity",
|
||||||
side_effect=sync.blocking_finish,
|
side_effect=sync.blocking_discontinuity,
|
||||||
autospec=True,
|
autospec=True,
|
||||||
):
|
):
|
||||||
yield sync
|
yield sync
|
||||||
|
@ -448,3 +448,33 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy
|
|||||||
|
|
||||||
stream_worker_sync.resume()
|
stream_worker_sync.resume()
|
||||||
stream.stop()
|
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