Merge pull request #59555 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2021-11-11 13:48:35 -08:00 committed by GitHub
commit 6f3f16dbc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 131 additions and 23 deletions

View File

@ -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",

View File

@ -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)
): ):

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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}"

View File

@ -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."""

View File

@ -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."""

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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.")

View File

@ -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

View File

@ -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()