diff --git a/CODEOWNERS b/CODEOWNERS index e5b6ed9798a..88669e8b5c7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -264,6 +264,7 @@ tests/components/enphase_envoy/* @gtdiehl homeassistant/components/entur_public_transport/* @hfurubotten homeassistant/components/environment_canada/* @gwww @michaeldavie tests/components/environment_canada/* @gwww @michaeldavie +homeassistant/components/envisalink/* @ufodone homeassistant/components/ephember/* @ttroy50 homeassistant/components/epson/* @pszafer tests/components/epson/* @pszafer diff --git a/homeassistant/components/aseko_pool_live/manifest.json b/homeassistant/components/aseko_pool_live/manifest.json index f6323b49354..d1fc553a9ff 100644 --- a/homeassistant/components/aseko_pool_live/manifest.json +++ b/homeassistant/components/aseko_pool_live/manifest.json @@ -3,7 +3,7 @@ "name": "Aseko Pool Live", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aseko_pool_live", - "requirements": ["aioaseko==0.0.1"], + "requirements": ["aioaseko==0.0.2"], "codeowners": [ "@milanmeu" ], diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 9b340096fde..031f513843f 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -75,6 +75,7 @@ async def async_setup_august( hass.config_entries.async_update_entry(config_entry, data=config_data) await august_gateway.async_authenticate() + await august_gateway.async_refresh_access_token_if_needed() hass.data.setdefault(DOMAIN, {}) data = hass.data[DOMAIN][config_entry.entry_id] = { @@ -106,11 +107,10 @@ class AugustData(AugustSubscriberMixin): async def async_setup(self): """Async setup of august device data and activities.""" token = self._august_gateway.access_token - user_data, locks, doorbells = await asyncio.gather( - self._api.async_get_user(token), - self._api.async_get_operable_locks(token), - self._api.async_get_doorbells(token), - ) + # This used to be a gather but it was less reliable with august's recent api changes. + user_data = await self._api.async_get_user(token) + locks = await self._api.async_get_operable_locks(token) + doorbells = await self._api.async_get_doorbells(token) if not doorbells: doorbells = [] if not locks: diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index ea6769fabcf..0dfcb6094b1 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.20"], + "requirements": ["yalexs==1.1.22"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 110cf11cde9..466dce1c84d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -222,7 +222,12 @@ async def async_get_mjpeg_stream( """Fetch an mjpeg stream from a camera entity.""" camera = _get_camera_from_entity_id(hass, entity_id) - return await camera.handle_async_mjpeg_stream(request) + try: + stream = await camera.handle_async_mjpeg_stream(request) + except ConnectionResetError: + stream = None + _LOGGER.debug("Error while writing MJPEG stream to transport") + return stream async def async_get_still_stream( @@ -784,7 +789,11 @@ class CameraMjpegStream(CameraView): async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse: """Serve camera stream, possibly with interval.""" if (interval_str := request.query.get("interval")) is None: - stream = await camera.handle_async_mjpeg_stream(request) + try: + stream = await camera.handle_async_mjpeg_stream(request) + except ConnectionResetError: + stream = None + _LOGGER.debug("Error while writing MJPEG stream to transport") if stream is None: raise web.HTTPBadGateway() return stream diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 86c02ae9ee9..99b23834549 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -81,8 +81,8 @@ class CPUSpeedSensor(SensorEntity): if info: self._attr_extra_state_attributes = { - ATTR_ARCH: info["arch_string_raw"], - ATTR_BRAND: info["brand_raw"], + ATTR_ARCH: info.get("arch_string_raw"), + ATTR_BRAND: info.get("brand_raw"), } if HZ_ADVERTISED in info: self._attr_extra_state_attributes[ATTR_HZ] = round( diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 183627fdfa6..aa276af492c 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -137,6 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: keep_alive, hass.loop, connection_timeout, + False, ) hass.data[DATA_EVL] = controller @@ -181,12 +182,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.debug("The envisalink sent a partition update event") async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data) - @callback - def async_zone_bypass_update(data): - """Handle zone bypass status updates.""" - _LOGGER.debug("Envisalink sent a zone bypass update event. Updating zones") - async_dispatcher_send(hass, SIGNAL_ZONE_BYPASS_UPDATE, data) - @callback def stop_envisalink(event): """Shutdown envisalink connection and thread on exit.""" @@ -206,7 +201,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: controller.callback_login_failure = async_login_fail_callback controller.callback_login_timeout = async_connection_fail_callback controller.callback_login_success = async_connection_success_callback - controller.callback_zone_bypass_update = async_zone_bypass_update _LOGGER.info("Start envisalink") controller.start() @@ -240,13 +234,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, Platform.BINARY_SENSOR, "envisalink", {CONF_ZONES: zones}, config ) ) - # Only DSC panels support getting zone bypass status - if panel_type == PANEL_TYPE_DSC: - hass.async_create_task( - async_load_platform( - hass, "switch", "envisalink", {CONF_ZONES: zones}, config - ) - ) + + # Zone bypass switches are not currently created due to an issue with some panels. + # These switches will be re-added in the future after some further refactoring of the integration. hass.services.async_register( DOMAIN, SERVICE_CUSTOM_FUNCTION, handle_custom_function, schema=SERVICE_SCHEMA diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 25290a5d431..52ac06ff8c3 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -2,7 +2,7 @@ "domain": "envisalink", "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", - "requirements": ["pyenvisalink==4.3"], - "codeowners": [], + "requirements": ["pyenvisalink==4.4"], + "codeowners": ["@ufodone"], "iot_class": "local_push" } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 042bf930d0e..25e3abe9700 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==10.8.1"], + "requirements": ["aioesphomeapi==10.8.2"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index a98f75fceb8..a4c550b8119 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -57,6 +57,8 @@ KEY_POSITION = "position" DEFAULT_NAME = "Cover Group" +# No limit on parallel updates to enable a group calling another group +PARALLEL_UPDATES = 0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index cef30dc3c69..7920e0f5d20 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -52,6 +52,8 @@ SUPPORTED_FLAGS = {SUPPORT_SET_SPEED, SUPPORT_DIRECTION, SUPPORT_OSCILLATE} DEFAULT_NAME = "Fan Group" +# No limit on parallel updates to enable a group calling another group +PARALLEL_UPDATES = 0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 201156db600..ea74136b204 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -58,6 +58,9 @@ from .util import find_state_attributes, mean_tuple, reduce_attribute DEFAULT_NAME = "Light Group" +# No limit on parallel updates to enable a group calling another group +PARALLEL_UPDATES = 0 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 3d4851d8852..056eacb6a5b 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -191,6 +191,8 @@ def parse_mapping(mapping, parents=None): def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 """Set up the CEC capability.""" + hass.data[DOMAIN] = {} + # Parse configuration into a dict of device name to physical address # represented as a list of four elements. device_aliases = {} diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 546bb055ca8..0408f547f25 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -138,6 +138,11 @@ async def async_setup_entry( devices = bridge.get_devices() bridge_device = devices[BRIDGE_DEVICE_ID] + if not config_entry.unique_id: + hass.config_entries.async_update_entry( + config_entry, unique_id=hex(bridge_device["serial"])[2:].zfill(8) + ) + buttons = bridge.buttons _async_register_bridge_device(hass, entry_id, bridge_device) button_devices = _async_register_button_devices( diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 8636bd6ed94..fc664910c84 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.5.10"], + "requirements": ["motionblinds==0.5.12"], "dependencies": ["network"], "codeowners": ["@starkillerOG"], "iot_class": "local_push" diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 478e608700c..832c186db35 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==1.6.0"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.7.1"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nina/const.py b/homeassistant/components/nina/const.py index 12df703a480..18af5021544 100644 --- a/homeassistant/components/nina/const.py +++ b/homeassistant/components/nina/const.py @@ -26,7 +26,7 @@ CONST_LIST_E_TO_H: list[str] = ["E", "F", "G", "H"] CONST_LIST_I_TO_L: list[str] = ["I", "J", "K", "L"] CONST_LIST_M_TO_Q: list[str] = ["M", "N", "O", "Ö", "P", "Q"] CONST_LIST_R_TO_U: list[str] = ["R", "S", "T", "U", "Ü"] -CONST_LIST_V_TO_Z: list[str] = ["V", "W", "X", "Y"] +CONST_LIST_V_TO_Z: list[str] = ["V", "W", "X", "Y", "Z"] CONST_REGION_A_TO_D: Final = "_a_to_d" CONST_REGION_E_TO_H: Final = "_e_to_h" diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 1a3c6b52a0d..be24be632fc 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -82,7 +82,7 @@ async def async_setup_entry( class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): """Representation of a Philips TV exposing the JointSpace API.""" - _coordinator: PhilipsTVDataUpdateCoordinator + coordinator: PhilipsTVDataUpdateCoordinator _attr_device_class = MediaPlayerDeviceClass.TV def __init__( @@ -91,7 +91,6 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): ) -> None: """Initialize the Philips TV.""" self._tv = coordinator.api - self._coordinator = coordinator self._sources = {} self._channels = {} self._supports = SUPPORT_PHILIPS_JS @@ -125,7 +124,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): def supported_features(self): """Flag media player features that are supported.""" supports = self._supports - if self._coordinator.turn_on or ( + if self.coordinator.turn_on or ( self._tv.on and self._tv.powerstate is not None ): supports |= SUPPORT_TURN_ON @@ -170,7 +169,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): await self._tv.setPowerState("On") self._state = STATE_ON else: - await self._coordinator.turn_on.async_run(self.hass, self._context) + await self.coordinator.turn_on.async_run(self.hass, self._context) await self._async_update_soon() async def async_turn_off(self): diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 6bf60f7f5b0..09fe16215b6 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -30,7 +30,7 @@ async def async_setup_entry( class PhilipsTVRemote(CoordinatorEntity, RemoteEntity): """Device that sends commands.""" - _coordinator: PhilipsTVDataUpdateCoordinator + coordinator: PhilipsTVDataUpdateCoordinator def __init__( self, @@ -63,7 +63,7 @@ class PhilipsTVRemote(CoordinatorEntity, RemoteEntity): if self._tv.on and self._tv.powerstate: await self._tv.setPowerState("On") else: - await self._coordinator.turn_on.async_run(self.hass, self._context) + await self.coordinator.turn_on.async_run(self.hass, self._context) self.async_write_ha_state() async def async_turn_off(self, **kwargs): diff --git a/homeassistant/components/pvoutput/config_flow.py b/homeassistant/components/pvoutput/config_flow.py index a1933ff9315..53eabe225f6 100644 --- a/homeassistant/components/pvoutput/config_flow.py +++ b/homeassistant/components/pvoutput/config_flow.py @@ -23,7 +23,7 @@ async def validate_input(hass: HomeAssistant, *, api_key: str, system_id: int) - api_key=api_key, system_id=system_id, ) - await pvoutput.status() + await pvoutput.system() class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/pvoutput/coordinator.py b/homeassistant/components/pvoutput/coordinator.py index cadef8c8a0d..7b307f20274 100644 --- a/homeassistant/components/pvoutput/coordinator.py +++ b/homeassistant/components/pvoutput/coordinator.py @@ -1,14 +1,14 @@ """DataUpdateCoordinator for the PVOutput integration.""" from __future__ import annotations -from pvo import PVOutput, PVOutputAuthenticationError, Status +from pvo import PVOutput, PVOutputAuthenticationError, PVOutputNoDataError, Status from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_SYSTEM_ID, DOMAIN, LOGGER, SCAN_INTERVAL @@ -33,5 +33,7 @@ class PVOutputDataUpdateCoordinator(DataUpdateCoordinator[Status]): """Fetch system status from PVOutput.""" try: return await self.pvoutput.status() + except PVOutputNoDataError as err: + raise UpdateFailed("PVOutput has no data available") from err except PVOutputAuthenticationError as err: raise ConfigEntryAuthFailed from err diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index 042c6b9aa99..021fffe0e01 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/pvoutput", "config_flow": true, "codeowners": ["@fabaff", "@frenck"], - "requirements": ["pvo==0.2.1"], + "requirements": ["pvo==0.2.2"], "iot_class": "cloud_polling", "quality_scale": "platinum" } diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py index 3d28086db43..8f4a8b0aca4 100644 --- a/homeassistant/components/raspihats/__init__.py +++ b/homeassistant/components/raspihats/__init__.py @@ -40,7 +40,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" ) - hass.data[DOMAIN][I2C_HATS_MANAGER] = I2CHatsManager() + hass.data[DOMAIN] = {I2C_HATS_MANAGER: I2CHatsManager()} def start_i2c_hats_keep_alive(event): """Start I2C-HATs keep alive.""" diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index abb0696360b..6aec401b412 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -44,6 +44,7 @@ SONOS_ALBUM_ARTIST = "album_artists" SONOS_TRACKS = "tracks" SONOS_COMPOSER = "composers" SONOS_RADIO = "radio" +SONOS_OTHER_ITEM = "other items" SONOS_STATE_PLAYING = "PLAYING" SONOS_STATE_TRANSITIONING = "TRANSITIONING" @@ -76,6 +77,7 @@ SONOS_TO_MEDIA_CLASSES = { "object.container.person.musicArtist": MEDIA_CLASS_ARTIST, "object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST, "object.container.playlistContainer": MEDIA_CLASS_PLAYLIST, + "object.item": MEDIA_CLASS_TRACK, "object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK, "object.item.audioItem.audioBroadcast": MEDIA_CLASS_GENRE, } @@ -121,6 +123,7 @@ SONOS_TYPES_MAPPING = { "object.container.person.musicArtist": SONOS_ALBUM_ARTIST, "object.container.playlistContainer.sameArtist": SONOS_ARTIST, "object.container.playlistContainer": SONOS_PLAYLISTS, + "object.item": SONOS_OTHER_ITEM, "object.item.audioItem.musicTrack": SONOS_TRACKS, "object.item.audioItem.audioBroadcast": SONOS_RADIO, } diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 2e3bf9d1fcb..2d971469928 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -162,8 +162,17 @@ def build_item_response(media_library, payload, get_thumbnail_url=None): payload["idstring"].split("/")[2:] ) + try: + search_type = MEDIA_TYPES_TO_SONOS[payload["search_type"]] + except KeyError: + _LOGGER.debug( + "Unknown media type received when building item response: %s", + payload["search_type"], + ) + return + media = media_library.browse_by_idstring( - MEDIA_TYPES_TO_SONOS[payload["search_type"]], + search_type, payload["idstring"], full_album_art_uri=True, max_items=0, @@ -371,11 +380,16 @@ def favorites_payload(favorites): group_types = {fav.reference.item_class for fav in favorites} for group_type in sorted(group_types): - media_content_type = SONOS_TYPES_MAPPING[group_type] + try: + media_content_type = SONOS_TYPES_MAPPING[group_type] + media_class = SONOS_TO_MEDIA_CLASSES[group_type] + except KeyError: + _LOGGER.debug("Unknown media type or class received %s", group_type) + continue children.append( BrowseMedia( title=media_content_type.title(), - media_class=SONOS_TO_MEDIA_CLASSES[group_type], + media_class=media_class, media_content_id=group_type, media_content_type="favorites_folder", can_play=False, diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 5c36a0c71c3..c3e9504c05c 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -1,6 +1,11 @@ """The spotify integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any import aiohttp +import requests from spotipy import Spotify, SpotifyException import voluptuous as vol @@ -20,13 +25,16 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( async_get_config_entry_implementation, ) from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from . import config_flow from .const import ( DATA_SPOTIFY_CLIENT, + DATA_SPOTIFY_DEVICES, DATA_SPOTIFY_ME, DATA_SPOTIFY_SESSION, DOMAIN, + LOGGER, MEDIA_PLAYER_PREFIX, SPOTIFY_SCOPES, ) @@ -112,9 +120,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SpotifyException as err: raise ConfigEntryNotReady from err + async def _update_devices() -> list[dict[str, Any]]: + try: + devices: dict[str, Any] | None = await hass.async_add_executor_job( + spotify.devices + ) + except (requests.RequestException, SpotifyException) as err: + raise UpdateFailed from err + + if devices is None: + return [] + + return devices.get("devices", []) + + device_coordinator: DataUpdateCoordinator[ + list[dict[str, Any]] + ] = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{entry.title} Devices", + update_interval=timedelta(minutes=5), + update_method=_update_devices, + ) + await device_coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_SPOTIFY_CLIENT: spotify, + DATA_SPOTIFY_DEVICES: device_coordinator, DATA_SPOTIFY_ME: current_user, DATA_SPOTIFY_SESSION: session, } diff --git a/homeassistant/components/spotify/const.py b/homeassistant/components/spotify/const.py index 7978ac8712f..0ed7cd2412e 100644 --- a/homeassistant/components/spotify/const.py +++ b/homeassistant/components/spotify/const.py @@ -1,8 +1,13 @@ """Define constants for the Spotify integration.""" +import logging + DOMAIN = "spotify" +LOGGER = logging.getLogger(__package__) + DATA_SPOTIFY_CLIENT = "spotify_client" +DATA_SPOTIFY_DEVICES = "spotify_devices" DATA_SPOTIFY_ME = "spotify_me" DATA_SPOTIFY_SESSION = "spotify_session" diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index e279b150883..b3bb2efd1c0 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -52,7 +52,7 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.device_registry import DeviceEntryType @@ -62,6 +62,7 @@ from homeassistant.util.dt import utc_from_timestamp from .const import ( DATA_SPOTIFY_CLIENT, + DATA_SPOTIFY_DEVICES, DATA_SPOTIFY_ME, DATA_SPOTIFY_SESSION, DOMAIN, @@ -269,7 +270,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): ) self._currently_playing: dict | None = {} - self._devices: list[dict] | None = [] self._playlist: dict | None = None self._attr_name = self._name @@ -290,6 +290,11 @@ class SpotifyMediaPlayer(MediaPlayerEntity): """Return spotify API.""" return self._spotify_data[DATA_SPOTIFY_CLIENT] + @property + def _devices(self) -> list: + """Return spotify devices.""" + return self._spotify_data[DATA_SPOTIFY_DEVICES].data + @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" @@ -517,13 +522,13 @@ class SpotifyMediaPlayer(MediaPlayerEntity): current = self._spotify.current_playback() self._currently_playing = current or {} - self._playlist = None context = self._currently_playing.get("context") - if context is not None and context["type"] == MEDIA_TYPE_PLAYLIST: - self._playlist = self._spotify.playlist(current["context"]["uri"]) - - devices = self._spotify.devices() or {} - self._devices = devices.get("devices", []) + if context is not None and ( + self._playlist is None or self._playlist["uri"] != context["uri"] + ): + self._playlist = None + if context["type"] == MEDIA_TYPE_PLAYLIST: + self._playlist = self._spotify.playlist(current["context"]["uri"]) async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" @@ -543,6 +548,22 @@ class SpotifyMediaPlayer(MediaPlayerEntity): media_content_id, ) + @callback + def _handle_devices_update(self) -> None: + """Handle updated data from the coordinator.""" + if not self.enabled: + return + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + self._spotify_data[DATA_SPOTIFY_DEVICES].async_add_listener( + self._handle_devices_update + ) + ) + async def async_browse_media_internal( hass, diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index b6bfd2122ba..79506c0bda2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -338,7 +338,6 @@ class Stream: ) except StreamWorkerError as err: self._logger.error("Error from stream worker: %s", str(err)) - self._available = False stream_state.discontinuity() if not self.keepalive or self._thread_quit.is_set(): diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index c436557cea9..124679c3516 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==1.0.5"], + "requirements": ["py-synologydsm-api==1.0.6"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/const.py b/homeassistant/const.py index 75cd20528bb..13250b3f9e5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "5" +PATCH_VERSION: Final = "6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index b670c734b47..e9038d1f658 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -223,7 +223,10 @@ def convert_to_entity_category( "EntityCategory instead" % (type(value).__name__, value), error_if_core=False, ) - return EntityCategory(value) + try: + return EntityCategory(value) + except ValueError: + return None return value diff --git a/requirements_all.txt b/requirements_all.txt index a50bc68b199..b524400e811 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -138,7 +138,7 @@ aio_georss_gdacs==0.5 aioambient==2021.11.0 # homeassistant.components.aseko_pool_live -aioaseko==0.0.1 +aioaseko==0.0.2 # homeassistant.components.asuswrt aioasuswrt==1.4.0 @@ -166,7 +166,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.8.1 +aioesphomeapi==10.8.2 # homeassistant.components.flo aioflo==2021.11.0 @@ -764,7 +764,7 @@ google-cloud-pubsub==2.9.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==1.6.0 +google-nest-sdm==1.7.1 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -1049,7 +1049,7 @@ minio==5.0.10 mitemp_bt==0.0.5 # homeassistant.components.motion_blinds -motionblinds==0.5.10 +motionblinds==0.5.12 # homeassistant.components.motioneye motioneye-client==0.3.12 @@ -1310,7 +1310,7 @@ pushbullet.py==0.11.0 pushover_complete==1.1.1 # homeassistant.components.pvoutput -pvo==0.2.1 +pvo==0.2.2 # homeassistant.components.rpi_gpio_pwm pwmled==1.6.7 @@ -1331,7 +1331,7 @@ py-nightscout==1.2.2 py-schluter==0.1.7 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.5 +py-synologydsm-api==1.0.6 # homeassistant.components.zabbix py-zabbix==1.1.7 @@ -1500,7 +1500,7 @@ pyeight==0.2.0 pyemby==1.8 # homeassistant.components.envisalink -pyenvisalink==4.3 +pyenvisalink==4.4 # homeassistant.components.ephember pyephember==0.3.1 @@ -1954,7 +1954,7 @@ python-mpd2==3.0.4 python-mystrom==1.1.2 # homeassistant.components.nest -python-nest==4.1.0 +python-nest==4.2.0 # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 @@ -2513,7 +2513,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.7 # homeassistant.components.august -yalexs==1.1.20 +yalexs==1.1.22 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5bd1a6443c..358a1d21748 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ aio_georss_gdacs==0.5 aioambient==2021.11.0 # homeassistant.components.aseko_pool_live -aioaseko==0.0.1 +aioaseko==0.0.2 # homeassistant.components.asuswrt aioasuswrt==1.4.0 @@ -119,7 +119,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.8.1 +aioesphomeapi==10.8.2 # homeassistant.components.flo aioflo==2021.11.0 @@ -492,7 +492,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.9.0 # homeassistant.components.nest -google-nest-sdm==1.6.0 +google-nest-sdm==1.7.1 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -655,7 +655,7 @@ millheater==0.9.0 minio==5.0.10 # homeassistant.components.motion_blinds -motionblinds==0.5.10 +motionblinds==0.5.12 # homeassistant.components.motioneye motioneye-client==0.3.12 @@ -814,7 +814,7 @@ pure-python-adb[async]==0.3.0.dev0 pushbullet.py==0.11.0 # homeassistant.components.pvoutput -pvo==0.2.1 +pvo==0.2.2 # homeassistant.components.canary py-canary==0.5.1 @@ -829,7 +829,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.5 +py-synologydsm-api==1.0.6 # homeassistant.components.seventeentrack py17track==2021.12.2 @@ -1206,7 +1206,7 @@ python-kasa==0.4.1 python-miio==0.5.9.2 # homeassistant.components.nest -python-nest==4.1.0 +python-nest==4.2.0 # homeassistant.components.ozw python-openzwave-mqtt[mqtt-client]==1.4.0 @@ -1541,7 +1541,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.7 # homeassistant.components.august -yalexs==1.1.20 +yalexs==1.1.22 # homeassistant.components.yeelight yeelight==0.7.8 diff --git a/setup.cfg b/setup.cfg index 7d73060f322..c912d68ed55 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.5 +version = 2022.2.6 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 diff --git a/tests/components/cpuspeed/test_sensor.py b/tests/components/cpuspeed/test_sensor.py index 134d19b31ea..ebf9f0111bd 100644 --- a/tests/components/cpuspeed/test_sensor.py +++ b/tests/components/cpuspeed/test_sensor.py @@ -61,3 +61,25 @@ async def test_sensor( assert state.attributes.get(ATTR_ARCH) == "aargh" assert state.attributes.get(ATTR_BRAND) == "Intel Ryzen 7" assert state.attributes.get(ATTR_HZ) == 3.6 + + +async def test_sensor_partial_info( + hass: HomeAssistant, + mock_cpuinfo: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the CPU Speed sensor missing info.""" + mock_config_entry.add_to_hass(hass) + + # Pop some info from the mocked CPUSpeed + mock_cpuinfo.return_value.pop("brand_raw") + mock_cpuinfo.return_value.pop("arch_string_raw") + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.cpu_speed") + assert state + assert state.state == "3.2" + assert state.attributes.get(ATTR_ARCH) is None + assert state.attributes.get(ATTR_BRAND) is None diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index cf1fba992e7..d090141a9d2 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -1,6 +1,7 @@ """The tests for the group cover platform.""" from datetime import timedelta +import async_timeout import pytest from homeassistant.components.cover import ( @@ -735,3 +736,52 @@ async def test_is_opening_closing(hass, setup_comp): assert hass.states.get(DEMO_COVER_TILT).state == STATE_OPENING assert hass.states.get(DEMO_COVER_POS).state == STATE_OPEN assert hass.states.get(COVER_GROUP).state == STATE_OPENING + + +async def test_nested_group(hass): + """Test nested cover group.""" + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + {"platform": "demo"}, + { + "platform": "group", + "entities": ["cover.bedroom_group"], + "name": "Nested Group", + }, + { + "platform": "group", + CONF_ENTITIES: [DEMO_COVER_POS, DEMO_COVER_TILT], + "name": "Bedroom Group", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("cover.bedroom_group") + assert state is not None + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ENTITY_ID) == [DEMO_COVER_POS, DEMO_COVER_TILT] + + state = hass.states.get("cover.nested_group") + assert state is not None + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ENTITY_ID) == ["cover.bedroom_group"] + + # Test controlling the nested group + async with async_timeout.timeout(0.5): + await hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.nested_group"}, + blocking=True, + ) + assert hass.states.get(DEMO_COVER_POS).state == STATE_CLOSING + assert hass.states.get(DEMO_COVER_TILT).state == STATE_CLOSING + assert hass.states.get("cover.bedroom_group").state == STATE_CLOSING + assert hass.states.get("cover.nested_group").state == STATE_CLOSING diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index abb1dcf245a..19b4fe4670a 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -1,6 +1,7 @@ """The tests for the group fan platform.""" from unittest.mock import patch +import async_timeout import pytest from homeassistant import config as hass_config @@ -497,3 +498,58 @@ async def test_service_calls(hass, setup_comp): assert percentage_full_fan_state.attributes[ATTR_DIRECTION] == DIRECTION_REVERSE fan_group_state = hass.states.get(FAN_GROUP) assert fan_group_state.attributes[ATTR_DIRECTION] == DIRECTION_REVERSE + + +async def test_nested_group(hass): + """Test nested fan group.""" + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + {"platform": "demo"}, + { + "platform": "group", + "entities": ["fan.bedroom_group"], + "name": "Nested Group", + }, + { + "platform": "group", + CONF_ENTITIES: [ + LIVING_ROOM_FAN_ENTITY_ID, + PERCENTAGE_FULL_FAN_ENTITY_ID, + ], + "name": "Bedroom Group", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("fan.bedroom_group") + assert state is not None + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ENTITY_ID) == [ + LIVING_ROOM_FAN_ENTITY_ID, + PERCENTAGE_FULL_FAN_ENTITY_ID, + ] + + state = hass.states.get("fan.nested_group") + assert state is not None + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ENTITY_ID) == ["fan.bedroom_group"] + + # Test controlling the nested group + async with async_timeout.timeout(0.5): + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.nested_group"}, + blocking=True, + ) + assert hass.states.get(LIVING_ROOM_FAN_ENTITY_ID).state == STATE_ON + assert hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID).state == STATE_ON + assert hass.states.get("fan.bedroom_group").state == STATE_ON + assert hass.states.get("fan.nested_group").state == STATE_ON diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 843f15c7113..d356b20b40f 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -2,6 +2,7 @@ import unittest.mock from unittest.mock import MagicMock, patch +import async_timeout import pytest from homeassistant import config as hass_config @@ -1470,12 +1471,12 @@ async def test_reload_with_base_integration_platform_not_setup(hass): async def test_nested_group(hass): """Test nested light group.""" - hass.states.async_set("light.kitchen", "on") await async_setup_component( hass, LIGHT_DOMAIN, { LIGHT_DOMAIN: [ + {"platform": "demo"}, { "platform": DOMAIN, "entities": ["light.bedroom_group"], @@ -1483,7 +1484,7 @@ async def test_nested_group(hass): }, { "platform": DOMAIN, - "entities": ["light.kitchen", "light.bedroom"], + "entities": ["light.bed_light", "light.kitchen_lights"], "name": "Bedroom Group", }, ] @@ -1496,9 +1497,25 @@ async def test_nested_group(hass): state = hass.states.get("light.bedroom_group") assert state is not None assert state.state == STATE_ON - assert state.attributes.get(ATTR_ENTITY_ID) == ["light.kitchen", "light.bedroom"] + assert state.attributes.get(ATTR_ENTITY_ID) == [ + "light.bed_light", + "light.kitchen_lights", + ] state = hass.states.get("light.nested_group") assert state is not None assert state.state == STATE_ON assert state.attributes.get(ATTR_ENTITY_ID) == ["light.bedroom_group"] + + # Test controlling the nested group + async with async_timeout.timeout(0.5): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: "light.nested_group"}, + blocking=True, + ) + assert hass.states.get("light.bed_light").state == STATE_OFF + assert hass.states.get("light.kitchen_lights").state == STATE_OFF + assert hass.states.get("light.bedroom_group").state == STATE_OFF + assert hass.states.get("light.nested_group").state == STATE_OFF diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index 27962297952..f741e2d1a84 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -1,6 +1,7 @@ """The tests for the Media group platform.""" from unittest.mock import patch +import async_timeout import pytest from homeassistant.components.group import DOMAIN @@ -486,12 +487,12 @@ async def test_service_calls(hass, mock_media_seek): async def test_nested_group(hass): """Test nested media group.""" - hass.states.async_set("media_player.player_1", "on") await async_setup_component( hass, MEDIA_DOMAIN, { MEDIA_DOMAIN: [ + {"platform": "demo"}, { "platform": DOMAIN, "entities": ["media_player.group_1"], @@ -499,7 +500,7 @@ async def test_nested_group(hass): }, { "platform": DOMAIN, - "entities": ["media_player.player_1", "media_player.player_2"], + "entities": ["media_player.bedroom", "media_player.kitchen"], "name": "Group 1", }, ] @@ -511,13 +512,28 @@ async def test_nested_group(hass): state = hass.states.get("media_player.group_1") assert state is not None - assert state.state == STATE_ON + assert state.state == STATE_PLAYING assert state.attributes.get(ATTR_ENTITY_ID) == [ - "media_player.player_1", - "media_player.player_2", + "media_player.bedroom", + "media_player.kitchen", ] state = hass.states.get("media_player.nested_group") assert state is not None - assert state.state == STATE_ON + assert state.state == STATE_PLAYING assert state.attributes.get(ATTR_ENTITY_ID) == ["media_player.group_1"] + + # Test controlling the nested group + async with async_timeout.timeout(0.5): + await hass.services.async_call( + MEDIA_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "media_player.group_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + assert hass.states.get("media_player.bedroom").state == STATE_OFF + assert hass.states.get("media_player.kitchen").state == STATE_OFF + assert hass.states.get("media_player.group_1").state == STATE_OFF + assert hass.states.get("media_player.nested_group").state == STATE_OFF diff --git a/tests/components/pvoutput/test_config_flow.py b/tests/components/pvoutput/test_config_flow.py index 0c060a75a9d..8cd776beea3 100644 --- a/tests/components/pvoutput/test_config_flow.py +++ b/tests/components/pvoutput/test_config_flow.py @@ -47,7 +47,7 @@ async def test_full_user_flow( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 async def test_full_flow_with_authentication_error( @@ -68,7 +68,7 @@ async def test_full_flow_with_authentication_error( assert result.get("step_id") == SOURCE_USER assert "flow_id" in result - mock_pvoutput_config_flow.status.side_effect = PVOutputAuthenticationError + mock_pvoutput_config_flow.system.side_effect = PVOutputAuthenticationError result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -83,9 +83,9 @@ async def test_full_flow_with_authentication_error( assert "flow_id" in result2 assert len(mock_setup_entry.mock_calls) == 0 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 - mock_pvoutput_config_flow.status.side_effect = None + mock_pvoutput_config_flow.system.side_effect = None result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={ @@ -102,14 +102,14 @@ async def test_full_flow_with_authentication_error( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 2 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 2 async def test_connection_error( hass: HomeAssistant, mock_pvoutput_config_flow: MagicMock ) -> None: """Test API connection error.""" - mock_pvoutput_config_flow.status.side_effect = PVOutputConnectionError + mock_pvoutput_config_flow.system.side_effect = PVOutputConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -123,7 +123,7 @@ async def test_connection_error( assert result.get("type") == RESULT_TYPE_FORM assert result.get("errors") == {"base": "cannot_connect"} - assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 async def test_already_configured( @@ -175,7 +175,7 @@ async def test_import_flow( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 async def test_reauth_flow( @@ -214,7 +214,7 @@ async def test_reauth_flow( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 async def test_reauth_with_authentication_error( @@ -243,7 +243,7 @@ async def test_reauth_with_authentication_error( assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result - mock_pvoutput_config_flow.status.side_effect = PVOutputAuthenticationError + mock_pvoutput_config_flow.system.side_effect = PVOutputAuthenticationError result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: "invalid_key"}, @@ -256,9 +256,9 @@ async def test_reauth_with_authentication_error( assert "flow_id" in result2 assert len(mock_setup_entry.mock_calls) == 0 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 1 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 - mock_pvoutput_config_flow.status.side_effect = None + mock_pvoutput_config_flow.system.side_effect = None result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={CONF_API_KEY: "valid_key"}, @@ -273,7 +273,7 @@ async def test_reauth_with_authentication_error( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_pvoutput_config_flow.status.mock_calls) == 2 + assert len(mock_pvoutput_config_flow.system.mock_calls) == 2 async def test_reauth_api_error( @@ -297,7 +297,7 @@ async def test_reauth_api_error( assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result - mock_pvoutput_config_flow.status.side_effect = PVOutputConnectionError + mock_pvoutput_config_flow.system.side_effect = PVOutputConnectionError result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: "some_new_key"}, diff --git a/tests/components/pvoutput/test_init.py b/tests/components/pvoutput/test_init.py index faaff3d4214..b583e0807e0 100644 --- a/tests/components/pvoutput/test_init.py +++ b/tests/components/pvoutput/test_init.py @@ -1,7 +1,11 @@ """Tests for the PVOutput integration.""" from unittest.mock import MagicMock -from pvo import PVOutputAuthenticationError, PVOutputConnectionError +from pvo import ( + PVOutputAuthenticationError, + PVOutputConnectionError, + PVOutputNoDataError, +) import pytest from homeassistant.components.pvoutput.const import CONF_SYSTEM_ID, DOMAIN @@ -35,13 +39,15 @@ async def test_load_unload_config_entry( assert mock_config_entry.state is ConfigEntryState.NOT_LOADED +@pytest.mark.parametrize("side_effect", [PVOutputConnectionError, PVOutputNoDataError]) async def test_config_entry_not_ready( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pvoutput: MagicMock, + side_effect: Exception, ) -> None: """Test the PVOutput configuration entry not ready.""" - mock_pvoutput.status.side_effect = PVOutputConnectionError + mock_pvoutput.status.side_effect = side_effect mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index f299177a08e..1eaac0e72bf 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1124,3 +1124,15 @@ async def test_deprecated_disabled_by_str(hass, registry, caplog): assert entry.disabled_by is er.RegistryEntryDisabler.USER assert " str for entity registry disabled_by. This is deprecated " in caplog.text + + +async def test_invalid_entity_category_str(hass, registry, caplog): + """Test use of invalid entity category.""" + entry = er.RegistryEntry( + "light", + "hue", + "5678", + entity_category="invalid", + ) + + assert entry.entity_category is None