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 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, {}) 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", 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/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/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 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/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) 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): 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 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"