From cca1b426bb2064d9bd01811b42a8bdc26f04b839 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Jun 2021 17:29:17 -0500 Subject: [PATCH 1/8] Fix Sonos battery sensors on S1 firmware (#51585) --- homeassistant/components/sonos/speaker.py | 31 ++++++++++++----------- tests/components/sonos/test_sensor.py | 13 ++++------ 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 58598648992..055b4ce6845 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -173,7 +173,7 @@ class SonosSpeaker: self.zone_name = speaker_info["zone_name"] # Battery - self.battery_info: dict[str, Any] | None = None + self.battery_info: dict[str, Any] = {} self._last_battery_event: datetime.datetime | None = None self._battery_poll_timer: Callable | None = None @@ -208,21 +208,15 @@ class SonosSpeaker: self.hass, f"{SONOS_SEEN}-{self.soco.uid}", self.async_seen ) - if (battery_info := fetch_battery_info_or_none(self.soco)) is None: - self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN}) - else: + if battery_info := fetch_battery_info_or_none(self.soco): self.battery_info = battery_info - # Only create a polling task if successful, may fail on S1 firmware - if battery_info: - # Battery events can be infrequent, polling is still necessary - self._battery_poll_timer = self.hass.helpers.event.track_time_interval( - self.async_poll_battery, BATTERY_SCAN_INTERVAL - ) - else: - _LOGGER.warning( - "S1 firmware detected, battery sensor may update infrequently" - ) + # Battery events can be infrequent, polling is still necessary + self._battery_poll_timer = self.hass.helpers.event.track_time_interval( + self.async_poll_battery, BATTERY_SCAN_INTERVAL + ) dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) + else: + self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN}) if new_alarms := self.update_alarms_for_speaker(): dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) @@ -386,7 +380,7 @@ class SonosSpeaker: async def async_update_device_properties(self, event: SonosEvent) -> None: """Update device properties from an event.""" - if (more_info := event.variables.get("more_info")) is not None: + if more_info := event.variables.get("more_info"): battery_dict = dict(x.split(":") for x in more_info.split(",")) await self.async_update_battery_info(battery_dict) self.async_write_entity_states() @@ -514,12 +508,19 @@ class SonosSpeaker: if not self._battery_poll_timer: # Battery info received for an S1 speaker + new_battery = not self.battery_info self.battery_info.update( { "Level": int(battery_dict["BattPct"]), "PowerSource": "EXTERNAL" if is_charging else "BATTERY", } ) + if new_battery: + _LOGGER.warning( + "S1 firmware detected on %s, battery info may update infrequently", + self.zone_name, + ) + async_dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) return if is_charging == self.charging: diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index c8910b481f3..12c12821a0d 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -3,7 +3,7 @@ from pysonos.exceptions import NotSupportedException from homeassistant.components.sonos import DOMAIN from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -68,21 +68,18 @@ async def test_battery_on_S1(hass, config_entry, config, soco, battery_event): entity_registry = await hass.helpers.entity_registry.async_get_registry() - battery = entity_registry.entities["sensor.zone_a_battery"] - battery_state = hass.states.get(battery.entity_id) - assert battery_state.state == STATE_UNAVAILABLE - - power = entity_registry.entities["binary_sensor.zone_a_power"] - power_state = hass.states.get(power.entity_id) - assert power_state.state == STATE_UNAVAILABLE + assert "sensor.zone_a_battery" not in entity_registry.entities + assert "binary_sensor.zone_a_power" not in entity_registry.entities # Update the speaker with a callback event sub_callback(battery_event) await hass.async_block_till_done() + battery = entity_registry.entities["sensor.zone_a_battery"] battery_state = hass.states.get(battery.entity_id) assert battery_state.state == "100" + power = entity_registry.entities["binary_sensor.zone_a_power"] power_state = hass.states.get(power.entity_id) assert power_state.state == STATE_OFF assert power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "BATTERY" From 3a5f51ed7d6eb798c6eb0aa97e221c6aad3ef71a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Jun 2021 20:17:14 -0500 Subject: [PATCH 2/8] Handle missing section ID for Plex clips (#51598) --- homeassistant/components/plex/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index 2dc7b83b439..406c263dbff 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -78,7 +78,7 @@ class PlexSession: if media.librarySectionID in SPECIAL_SECTIONS: self.media_library_title = SPECIAL_SECTIONS[media.librarySectionID] - elif media.librarySectionID < 1: + elif media.librarySectionID and media.librarySectionID < 1: self.media_library_title = UNKNOWN_SECTION _LOGGER.warning( "Unknown library section ID (%s) for title '%s', please create an issue", From 880fe82337e8599011b17dcfd646b62ebcd61ca7 Mon Sep 17 00:00:00 2001 From: blastoise186 <40033667+blastoise186@users.noreply.github.com> Date: Tue, 8 Jun 2021 13:20:15 +0100 Subject: [PATCH 3/8] Reduce ovo_energy polling rate to be less aggressive (#51613) * Reduce polling rate to be less aggressive The current polling rate is too aggressive for the purpose, this commit reduces it to 12 hours to play nice with OVO. * tweak polling to hourly --- homeassistant/components/ovo_energy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 18414db7292..79a8e6138eb 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -64,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name="sensor", update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=300), + update_interval=timedelta(seconds=3600), ) hass.data.setdefault(DOMAIN, {}) From cfea8a9ad1c10f6bc870a078da0852cb0ec2a7d9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 8 Jun 2021 13:23:25 +0200 Subject: [PATCH 4/8] Do not configure Shelly config entry created by custom component (#51616) --- homeassistant/components/shelly/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 8fc7cf6be23..3f75e290362 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -68,6 +68,18 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Shelly from a config entry.""" + # The custom component for Shelly devices uses shelly domain as well as core + # integration. If the user removes the custom component but doesn't remove the + # config entry, core integration will try to configure that config entry with an + # error. The config entry data for this custom component doesn't contain host + # value, so if host isn't present, config entry will not be configured. + if not entry.data.get(CONF_HOST): + _LOGGER.warning( + "The config entry %s probably comes from a custom integration, please remove it if you want to use core Shelly integration", + entry.title, + ) + return False + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {} hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None From 90d28e911c95558d11fd93c3c7f3034bfa7f1cba Mon Sep 17 00:00:00 2001 From: Pawel Date: Tue, 8 Jun 2021 20:01:36 +0200 Subject: [PATCH 5/8] Fix Onvif get_time_zone from device (#51620) Co-authored-by: Martin Hjelmare --- homeassistant/components/onvif/device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 35dc436d201..87b68508fa1 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -169,7 +169,9 @@ class ONVIFDevice: cdate = device_time.UTCDateTime else: tzone = ( - dt_util.get_time_zone(device_time.TimeZone) + dt_util.get_time_zone( + device_time.TimeZone or str(dt_util.DEFAULT_TIME_ZONE) + ) or dt_util.DEFAULT_TIME_ZONE ) cdate = device_time.LocalDateTime From 60b89101e5f359f6ee8c7fe42c68f664c47f2035 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Jun 2021 07:23:00 -1000 Subject: [PATCH 6/8] Ensure samsungtv reloads after reauth (#51714) * Ensure samsungtv reloads after reauth - Fixes a case of I/O in the event loop * Ensure config entry is reloaded --- homeassistant/components/samsungtv/config_flow.py | 3 ++- tests/components/samsungtv/test_config_flow.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index a69a456df40..e29298da2eb 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -291,13 +291,14 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): bridge = SamsungTVBridge.get_bridge( self._reauth_entry.data[CONF_METHOD], self._reauth_entry.data[CONF_HOST] ) - result = bridge.try_connect() + result = await self.hass.async_add_executor_job(bridge.try_connect) if result == RESULT_SUCCESS: new_data = dict(self._reauth_entry.data) new_data[CONF_TOKEN] = bridge.token self.hass.config_entries.async_update_entry( self._reauth_entry, data=new_data ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") if result not in (RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT): return self.async_abort(reason=result) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 5b85ecf7048..1dd11fa5ad9 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -905,6 +905,8 @@ async def test_form_reauth_websocket(hass, remotews: Mock): """Test reauthenticate websocket.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) entry.add_to_hass(hass) + assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + result = await hass.config_entries.flow.async_init( DOMAIN, context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, @@ -920,6 +922,7 @@ async def test_form_reauth_websocket(hass, remotews: Mock): await hass.async_block_till_done() assert result2["type"] == "abort" assert result2["reason"] == "reauth_successful" + assert entry.state == config_entries.ConfigEntryState.LOADED async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock): From 548e847453edfbbf66659177ab1573b6670800a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Jun 2021 07:24:30 -1000 Subject: [PATCH 7/8] Fix race condition in samsungtv turn off (#51716) - The state would flip flop if the update happened before the TV had fully shutdown --- .../components/samsungtv/media_player.py | 17 +++++++++++++++-- tests/components/samsungtv/test_media_player.py | 12 ++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 5822bafcc55..7efdcdcd439 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -21,6 +21,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.helpers import entity_component import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.script import Script @@ -50,6 +51,13 @@ SUPPORT_SAMSUNGTV = ( | SUPPORT_PLAY_MEDIA ) +# Since the TV will take a few seconds to go to sleep +# and actually be seen as off, we need to wait just a bit +# more than the next scan interval +SCAN_INTERVAL_PLUS_OFF_TIME = entity_component.DEFAULT_SCAN_INTERVAL + timedelta( + seconds=5 +) + async def async_setup_entry(hass, entry, async_add_entities): """Set up the Samsung TV from a config entry.""" @@ -148,7 +156,12 @@ class SamsungTVDevice(MediaPlayerEntity): """Return the availability of the device.""" if self._auth_failed: return False - return self._state == STATE_ON or self._on_script or self._mac + return ( + self._state == STATE_ON + or self._on_script + or self._mac + or self._power_off_in_progress() + ) @property def device_info(self): @@ -187,7 +200,7 @@ class SamsungTVDevice(MediaPlayerEntity): def turn_off(self): """Turn off media player.""" - self._end_of_power_off = dt_util.utcnow() + timedelta(seconds=15) + self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME self.send_key("KEY_POWEROFF") # Force closing of remote session to provide instant UI feedback diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 02eceeaacb7..2cdd5cf56df 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -419,6 +419,18 @@ async def test_state_without_turnon(hass, remote): assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True ) + state = hass.states.get(ENTITY_ID_NOTURNON) + # Should be STATE_UNAVAILABLE after the timer expires + assert state.state == STATE_OFF + + next_update = dt_util.utcnow() + timedelta(seconds=20) + with patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError, + ), patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID_NOTURNON) # Should be STATE_UNAVAILABLE since there is no way to turn it back on assert state.state == STATE_UNAVAILABLE From 97e36cd3c4c95e872cfc68988e56291ae5bc195f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Jun 2021 21:42:27 -0700 Subject: [PATCH 8/8] Bumped version to 2021.6.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3b1fc4705a8..8eed701d74d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __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)