Merge pull request #66351 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-02-11 14:11:43 -08:00 committed by GitHub
commit fc2d30c993
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 372 additions and 100 deletions

View File

@ -264,6 +264,7 @@ tests/components/enphase_envoy/* @gtdiehl
homeassistant/components/entur_public_transport/* @hfurubotten homeassistant/components/entur_public_transport/* @hfurubotten
homeassistant/components/environment_canada/* @gwww @michaeldavie homeassistant/components/environment_canada/* @gwww @michaeldavie
tests/components/environment_canada/* @gwww @michaeldavie tests/components/environment_canada/* @gwww @michaeldavie
homeassistant/components/envisalink/* @ufodone
homeassistant/components/ephember/* @ttroy50 homeassistant/components/ephember/* @ttroy50
homeassistant/components/epson/* @pszafer homeassistant/components/epson/* @pszafer
tests/components/epson/* @pszafer tests/components/epson/* @pszafer

View File

@ -3,7 +3,7 @@
"name": "Aseko Pool Live", "name": "Aseko Pool Live",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live", "documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
"requirements": ["aioaseko==0.0.1"], "requirements": ["aioaseko==0.0.2"],
"codeowners": [ "codeowners": [
"@milanmeu" "@milanmeu"
], ],

View File

@ -75,6 +75,7 @@ async def async_setup_august(
hass.config_entries.async_update_entry(config_entry, data=config_data) hass.config_entries.async_update_entry(config_entry, data=config_data)
await august_gateway.async_authenticate() await august_gateway.async_authenticate()
await august_gateway.async_refresh_access_token_if_needed()
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
data = hass.data[DOMAIN][config_entry.entry_id] = { data = hass.data[DOMAIN][config_entry.entry_id] = {
@ -106,11 +107,10 @@ class AugustData(AugustSubscriberMixin):
async def async_setup(self): async def async_setup(self):
"""Async setup of august device data and activities.""" """Async setup of august device data and activities."""
token = self._august_gateway.access_token token = self._august_gateway.access_token
user_data, locks, doorbells = await asyncio.gather( # This used to be a gather but it was less reliable with august's recent api changes.
self._api.async_get_user(token), user_data = await self._api.async_get_user(token)
self._api.async_get_operable_locks(token), locks = await self._api.async_get_operable_locks(token)
self._api.async_get_doorbells(token), doorbells = await self._api.async_get_doorbells(token)
)
if not doorbells: if not doorbells:
doorbells = [] doorbells = []
if not locks: if not locks:

View File

@ -2,7 +2,7 @@
"domain": "august", "domain": "august",
"name": "August", "name": "August",
"documentation": "https://www.home-assistant.io/integrations/august", "documentation": "https://www.home-assistant.io/integrations/august",
"requirements": ["yalexs==1.1.20"], "requirements": ["yalexs==1.1.22"],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],
"dhcp": [ "dhcp": [
{ {

View File

@ -222,7 +222,12 @@ async def async_get_mjpeg_stream(
"""Fetch an mjpeg stream from a camera entity.""" """Fetch an mjpeg stream from a camera entity."""
camera = _get_camera_from_entity_id(hass, entity_id) 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( async def async_get_still_stream(
@ -784,7 +789,11 @@ class CameraMjpegStream(CameraView):
async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse: async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse:
"""Serve camera stream, possibly with interval.""" """Serve camera stream, possibly with interval."""
if (interval_str := request.query.get("interval")) is None: if (interval_str := request.query.get("interval")) is None:
try:
stream = await camera.handle_async_mjpeg_stream(request) 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: if stream is None:
raise web.HTTPBadGateway() raise web.HTTPBadGateway()
return stream return stream

View File

@ -81,8 +81,8 @@ class CPUSpeedSensor(SensorEntity):
if info: if info:
self._attr_extra_state_attributes = { self._attr_extra_state_attributes = {
ATTR_ARCH: info["arch_string_raw"], ATTR_ARCH: info.get("arch_string_raw"),
ATTR_BRAND: info["brand_raw"], ATTR_BRAND: info.get("brand_raw"),
} }
if HZ_ADVERTISED in info: if HZ_ADVERTISED in info:
self._attr_extra_state_attributes[ATTR_HZ] = round( self._attr_extra_state_attributes[ATTR_HZ] = round(

View File

@ -137,6 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
keep_alive, keep_alive,
hass.loop, hass.loop,
connection_timeout, connection_timeout,
False,
) )
hass.data[DATA_EVL] = controller 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") _LOGGER.debug("The envisalink sent a partition update event")
async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data) 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 @callback
def stop_envisalink(event): def stop_envisalink(event):
"""Shutdown envisalink connection and thread on exit.""" """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_failure = async_login_fail_callback
controller.callback_login_timeout = async_connection_fail_callback controller.callback_login_timeout = async_connection_fail_callback
controller.callback_login_success = async_connection_success_callback controller.callback_login_success = async_connection_success_callback
controller.callback_zone_bypass_update = async_zone_bypass_update
_LOGGER.info("Start envisalink") _LOGGER.info("Start envisalink")
controller.start() controller.start()
@ -240,13 +234,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass, Platform.BINARY_SENSOR, "envisalink", {CONF_ZONES: zones}, config hass, Platform.BINARY_SENSOR, "envisalink", {CONF_ZONES: zones}, config
) )
) )
# Only DSC panels support getting zone bypass status
if panel_type == PANEL_TYPE_DSC: # Zone bypass switches are not currently created due to an issue with some panels.
hass.async_create_task( # These switches will be re-added in the future after some further refactoring of the integration.
async_load_platform(
hass, "switch", "envisalink", {CONF_ZONES: zones}, config
)
)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_CUSTOM_FUNCTION, handle_custom_function, schema=SERVICE_SCHEMA DOMAIN, SERVICE_CUSTOM_FUNCTION, handle_custom_function, schema=SERVICE_SCHEMA

View File

@ -2,7 +2,7 @@
"domain": "envisalink", "domain": "envisalink",
"name": "Envisalink", "name": "Envisalink",
"documentation": "https://www.home-assistant.io/integrations/envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink",
"requirements": ["pyenvisalink==4.3"], "requirements": ["pyenvisalink==4.4"],
"codeowners": [], "codeowners": ["@ufodone"],
"iot_class": "local_push" "iot_class": "local_push"
} }

View File

@ -3,7 +3,7 @@
"name": "ESPHome", "name": "ESPHome",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/esphome", "documentation": "https://www.home-assistant.io/integrations/esphome",
"requirements": ["aioesphomeapi==10.8.1"], "requirements": ["aioesphomeapi==10.8.2"],
"zeroconf": ["_esphomelib._tcp.local."], "zeroconf": ["_esphomelib._tcp.local."],
"codeowners": ["@OttoWinter", "@jesserockz"], "codeowners": ["@OttoWinter", "@jesserockz"],
"after_dependencies": ["zeroconf", "tag"], "after_dependencies": ["zeroconf", "tag"],

View File

@ -57,6 +57,8 @@ KEY_POSITION = "position"
DEFAULT_NAME = "Cover Group" DEFAULT_NAME = "Cover Group"
# No limit on parallel updates to enable a group calling another group
PARALLEL_UPDATES = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {

View File

@ -52,6 +52,8 @@ SUPPORTED_FLAGS = {SUPPORT_SET_SPEED, SUPPORT_DIRECTION, SUPPORT_OSCILLATE}
DEFAULT_NAME = "Fan Group" DEFAULT_NAME = "Fan Group"
# No limit on parallel updates to enable a group calling another group
PARALLEL_UPDATES = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {

View File

@ -58,6 +58,9 @@ from .util import find_state_attributes, mean_tuple, reduce_attribute
DEFAULT_NAME = "Light Group" DEFAULT_NAME = "Light Group"
# No limit on parallel updates to enable a group calling another group
PARALLEL_UPDATES = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,

View File

@ -191,6 +191,8 @@ def parse_mapping(mapping, parents=None):
def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901
"""Set up the CEC capability.""" """Set up the CEC capability."""
hass.data[DOMAIN] = {}
# Parse configuration into a dict of device name to physical address # Parse configuration into a dict of device name to physical address
# represented as a list of four elements. # represented as a list of four elements.
device_aliases = {} device_aliases = {}

View File

@ -138,6 +138,11 @@ async def async_setup_entry(
devices = bridge.get_devices() devices = bridge.get_devices()
bridge_device = devices[BRIDGE_DEVICE_ID] 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 buttons = bridge.buttons
_async_register_bridge_device(hass, entry_id, bridge_device) _async_register_bridge_device(hass, entry_id, bridge_device)
button_devices = _async_register_button_devices( button_devices = _async_register_button_devices(

View File

@ -3,7 +3,7 @@
"name": "Motion Blinds", "name": "Motion Blinds",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/motion_blinds", "documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"requirements": ["motionblinds==0.5.10"], "requirements": ["motionblinds==0.5.12"],
"dependencies": ["network"], "dependencies": ["network"],
"codeowners": ["@starkillerOG"], "codeowners": ["@starkillerOG"],
"iot_class": "local_push" "iot_class": "local_push"

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"dependencies": ["ffmpeg", "http", "media_source"], "dependencies": ["ffmpeg", "http", "media_source"],
"documentation": "https://www.home-assistant.io/integrations/nest", "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"], "codeowners": ["@allenporter"],
"quality_scale": "platinum", "quality_scale": "platinum",
"dhcp": [ "dhcp": [

View File

@ -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_I_TO_L: list[str] = ["I", "J", "K", "L"]
CONST_LIST_M_TO_Q: list[str] = ["M", "N", "O", "Ö", "P", "Q"] 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_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_A_TO_D: Final = "_a_to_d"
CONST_REGION_E_TO_H: Final = "_e_to_h" CONST_REGION_E_TO_H: Final = "_e_to_h"

View File

@ -82,7 +82,7 @@ async def async_setup_entry(
class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
"""Representation of a Philips TV exposing the JointSpace API.""" """Representation of a Philips TV exposing the JointSpace API."""
_coordinator: PhilipsTVDataUpdateCoordinator coordinator: PhilipsTVDataUpdateCoordinator
_attr_device_class = MediaPlayerDeviceClass.TV _attr_device_class = MediaPlayerDeviceClass.TV
def __init__( def __init__(
@ -91,7 +91,6 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
) -> None: ) -> None:
"""Initialize the Philips TV.""" """Initialize the Philips TV."""
self._tv = coordinator.api self._tv = coordinator.api
self._coordinator = coordinator
self._sources = {} self._sources = {}
self._channels = {} self._channels = {}
self._supports = SUPPORT_PHILIPS_JS self._supports = SUPPORT_PHILIPS_JS
@ -125,7 +124,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
def supported_features(self): def supported_features(self):
"""Flag media player features that are supported.""" """Flag media player features that are supported."""
supports = self._supports 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 self._tv.on and self._tv.powerstate is not None
): ):
supports |= SUPPORT_TURN_ON supports |= SUPPORT_TURN_ON
@ -170,7 +169,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
await self._tv.setPowerState("On") await self._tv.setPowerState("On")
self._state = STATE_ON self._state = STATE_ON
else: 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() await self._async_update_soon()
async def async_turn_off(self): async def async_turn_off(self):

View File

@ -30,7 +30,7 @@ async def async_setup_entry(
class PhilipsTVRemote(CoordinatorEntity, RemoteEntity): class PhilipsTVRemote(CoordinatorEntity, RemoteEntity):
"""Device that sends commands.""" """Device that sends commands."""
_coordinator: PhilipsTVDataUpdateCoordinator coordinator: PhilipsTVDataUpdateCoordinator
def __init__( def __init__(
self, self,
@ -63,7 +63,7 @@ class PhilipsTVRemote(CoordinatorEntity, RemoteEntity):
if self._tv.on and self._tv.powerstate: if self._tv.on and self._tv.powerstate:
await self._tv.setPowerState("On") await self._tv.setPowerState("On")
else: 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() self.async_write_ha_state()
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):

View File

@ -23,7 +23,7 @@ async def validate_input(hass: HomeAssistant, *, api_key: str, system_id: int) -
api_key=api_key, api_key=api_key,
system_id=system_id, system_id=system_id,
) )
await pvoutput.status() await pvoutput.system()
class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN): class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN):

View File

@ -1,14 +1,14 @@
"""DataUpdateCoordinator for the PVOutput integration.""" """DataUpdateCoordinator for the PVOutput integration."""
from __future__ import annotations 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.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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 from .const import CONF_SYSTEM_ID, DOMAIN, LOGGER, SCAN_INTERVAL
@ -33,5 +33,7 @@ class PVOutputDataUpdateCoordinator(DataUpdateCoordinator[Status]):
"""Fetch system status from PVOutput.""" """Fetch system status from PVOutput."""
try: try:
return await self.pvoutput.status() return await self.pvoutput.status()
except PVOutputNoDataError as err:
raise UpdateFailed("PVOutput has no data available") from err
except PVOutputAuthenticationError as err: except PVOutputAuthenticationError as err:
raise ConfigEntryAuthFailed from err raise ConfigEntryAuthFailed from err

View File

@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/pvoutput", "documentation": "https://www.home-assistant.io/integrations/pvoutput",
"config_flow": true, "config_flow": true,
"codeowners": ["@fabaff", "@frenck"], "codeowners": ["@fabaff", "@frenck"],
"requirements": ["pvo==0.2.1"], "requirements": ["pvo==0.2.2"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "platinum" "quality_scale": "platinum"
} }

View File

@ -40,7 +40,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" "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): def start_i2c_hats_keep_alive(event):
"""Start I2C-HATs keep alive.""" """Start I2C-HATs keep alive."""

View File

@ -44,6 +44,7 @@ SONOS_ALBUM_ARTIST = "album_artists"
SONOS_TRACKS = "tracks" SONOS_TRACKS = "tracks"
SONOS_COMPOSER = "composers" SONOS_COMPOSER = "composers"
SONOS_RADIO = "radio" SONOS_RADIO = "radio"
SONOS_OTHER_ITEM = "other items"
SONOS_STATE_PLAYING = "PLAYING" SONOS_STATE_PLAYING = "PLAYING"
SONOS_STATE_TRANSITIONING = "TRANSITIONING" SONOS_STATE_TRANSITIONING = "TRANSITIONING"
@ -76,6 +77,7 @@ SONOS_TO_MEDIA_CLASSES = {
"object.container.person.musicArtist": MEDIA_CLASS_ARTIST, "object.container.person.musicArtist": MEDIA_CLASS_ARTIST,
"object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST, "object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST,
"object.container.playlistContainer": MEDIA_CLASS_PLAYLIST, "object.container.playlistContainer": MEDIA_CLASS_PLAYLIST,
"object.item": MEDIA_CLASS_TRACK,
"object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK, "object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK,
"object.item.audioItem.audioBroadcast": MEDIA_CLASS_GENRE, "object.item.audioItem.audioBroadcast": MEDIA_CLASS_GENRE,
} }
@ -121,6 +123,7 @@ SONOS_TYPES_MAPPING = {
"object.container.person.musicArtist": SONOS_ALBUM_ARTIST, "object.container.person.musicArtist": SONOS_ALBUM_ARTIST,
"object.container.playlistContainer.sameArtist": SONOS_ARTIST, "object.container.playlistContainer.sameArtist": SONOS_ARTIST,
"object.container.playlistContainer": SONOS_PLAYLISTS, "object.container.playlistContainer": SONOS_PLAYLISTS,
"object.item": SONOS_OTHER_ITEM,
"object.item.audioItem.musicTrack": SONOS_TRACKS, "object.item.audioItem.musicTrack": SONOS_TRACKS,
"object.item.audioItem.audioBroadcast": SONOS_RADIO, "object.item.audioItem.audioBroadcast": SONOS_RADIO,
} }

View File

@ -162,8 +162,17 @@ def build_item_response(media_library, payload, get_thumbnail_url=None):
payload["idstring"].split("/")[2:] 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 = media_library.browse_by_idstring(
MEDIA_TYPES_TO_SONOS[payload["search_type"]], search_type,
payload["idstring"], payload["idstring"],
full_album_art_uri=True, full_album_art_uri=True,
max_items=0, max_items=0,
@ -371,11 +380,16 @@ def favorites_payload(favorites):
group_types = {fav.reference.item_class for fav in favorites} group_types = {fav.reference.item_class for fav in favorites}
for group_type in sorted(group_types): for group_type in sorted(group_types):
try:
media_content_type = SONOS_TYPES_MAPPING[group_type] 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( children.append(
BrowseMedia( BrowseMedia(
title=media_content_type.title(), title=media_content_type.title(),
media_class=SONOS_TO_MEDIA_CLASSES[group_type], media_class=media_class,
media_content_id=group_type, media_content_id=group_type,
media_content_type="favorites_folder", media_content_type="favorites_folder",
can_play=False, can_play=False,

View File

@ -1,6 +1,11 @@
"""The spotify integration.""" """The spotify integration."""
from __future__ import annotations
from datetime import timedelta
from typing import Any
import aiohttp import aiohttp
import requests
from spotipy import Spotify, SpotifyException from spotipy import Spotify, SpotifyException
import voluptuous as vol import voluptuous as vol
@ -20,13 +25,16 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
async_get_config_entry_implementation, async_get_config_entry_implementation,
) )
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from . import config_flow from . import config_flow
from .const import ( from .const import (
DATA_SPOTIFY_CLIENT, DATA_SPOTIFY_CLIENT,
DATA_SPOTIFY_DEVICES,
DATA_SPOTIFY_ME, DATA_SPOTIFY_ME,
DATA_SPOTIFY_SESSION, DATA_SPOTIFY_SESSION,
DOMAIN, DOMAIN,
LOGGER,
MEDIA_PLAYER_PREFIX, MEDIA_PLAYER_PREFIX,
SPOTIFY_SCOPES, SPOTIFY_SCOPES,
) )
@ -112,9 +120,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except SpotifyException as err: except SpotifyException as err:
raise ConfigEntryNotReady from 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.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = { hass.data[DOMAIN][entry.entry_id] = {
DATA_SPOTIFY_CLIENT: spotify, DATA_SPOTIFY_CLIENT: spotify,
DATA_SPOTIFY_DEVICES: device_coordinator,
DATA_SPOTIFY_ME: current_user, DATA_SPOTIFY_ME: current_user,
DATA_SPOTIFY_SESSION: session, DATA_SPOTIFY_SESSION: session,
} }

View File

@ -1,8 +1,13 @@
"""Define constants for the Spotify integration.""" """Define constants for the Spotify integration."""
import logging
DOMAIN = "spotify" DOMAIN = "spotify"
LOGGER = logging.getLogger(__package__)
DATA_SPOTIFY_CLIENT = "spotify_client" DATA_SPOTIFY_CLIENT = "spotify_client"
DATA_SPOTIFY_DEVICES = "spotify_devices"
DATA_SPOTIFY_ME = "spotify_me" DATA_SPOTIFY_ME = "spotify_me"
DATA_SPOTIFY_SESSION = "spotify_session" DATA_SPOTIFY_SESSION = "spotify_session"

View File

@ -52,7 +52,7 @@ from homeassistant.const import (
STATE_PAUSED, STATE_PAUSED,
STATE_PLAYING, STATE_PLAYING,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType
@ -62,6 +62,7 @@ from homeassistant.util.dt import utc_from_timestamp
from .const import ( from .const import (
DATA_SPOTIFY_CLIENT, DATA_SPOTIFY_CLIENT,
DATA_SPOTIFY_DEVICES,
DATA_SPOTIFY_ME, DATA_SPOTIFY_ME,
DATA_SPOTIFY_SESSION, DATA_SPOTIFY_SESSION,
DOMAIN, DOMAIN,
@ -269,7 +270,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
) )
self._currently_playing: dict | None = {} self._currently_playing: dict | None = {}
self._devices: list[dict] | None = []
self._playlist: dict | None = None self._playlist: dict | None = None
self._attr_name = self._name self._attr_name = self._name
@ -290,6 +290,11 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
"""Return spotify API.""" """Return spotify API."""
return self._spotify_data[DATA_SPOTIFY_CLIENT] return self._spotify_data[DATA_SPOTIFY_CLIENT]
@property
def _devices(self) -> list:
"""Return spotify devices."""
return self._spotify_data[DATA_SPOTIFY_DEVICES].data
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return device information about this entity.""" """Return device information about this entity."""
@ -517,14 +522,14 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
current = self._spotify.current_playback() current = self._spotify.current_playback()
self._currently_playing = current or {} self._currently_playing = current or {}
self._playlist = None
context = self._currently_playing.get("context") context = self._currently_playing.get("context")
if context is not None and context["type"] == MEDIA_TYPE_PLAYLIST: 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"]) self._playlist = self._spotify.playlist(current["context"]["uri"])
devices = self._spotify.devices() or {}
self._devices = devices.get("devices", [])
async def async_browse_media(self, media_content_type=None, media_content_id=None): async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper.""" """Implement the websocket media browsing helper."""
@ -543,6 +548,22 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
media_content_id, 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( async def async_browse_media_internal(
hass, hass,

View File

@ -338,7 +338,6 @@ class Stream:
) )
except StreamWorkerError as err: except StreamWorkerError as err:
self._logger.error("Error from stream worker: %s", str(err)) self._logger.error("Error from stream worker: %s", str(err))
self._available = False
stream_state.discontinuity() stream_state.discontinuity()
if not self.keepalive or self._thread_quit.is_set(): if not self.keepalive or self._thread_quit.is_set():

View File

@ -2,7 +2,7 @@
"domain": "synology_dsm", "domain": "synology_dsm",
"name": "Synology DSM", "name": "Synology DSM",
"documentation": "https://www.home-assistant.io/integrations/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"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
"config_flow": true, "config_flow": true,
"ssdp": [ "ssdp": [

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 2 MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "5" PATCH_VERSION: Final = "6"
__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, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -223,7 +223,10 @@ def convert_to_entity_category(
"EntityCategory instead" % (type(value).__name__, value), "EntityCategory instead" % (type(value).__name__, value),
error_if_core=False, error_if_core=False,
) )
try:
return EntityCategory(value) return EntityCategory(value)
except ValueError:
return None
return value return value

View File

@ -138,7 +138,7 @@ aio_georss_gdacs==0.5
aioambient==2021.11.0 aioambient==2021.11.0
# homeassistant.components.aseko_pool_live # homeassistant.components.aseko_pool_live
aioaseko==0.0.1 aioaseko==0.0.2
# homeassistant.components.asuswrt # homeassistant.components.asuswrt
aioasuswrt==1.4.0 aioasuswrt==1.4.0
@ -166,7 +166,7 @@ aioeagle==1.1.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==10.8.1 aioesphomeapi==10.8.2
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -764,7 +764,7 @@ google-cloud-pubsub==2.9.0
google-cloud-texttospeech==0.4.0 google-cloud-texttospeech==0.4.0
# homeassistant.components.nest # homeassistant.components.nest
google-nest-sdm==1.6.0 google-nest-sdm==1.7.1
# homeassistant.components.google_travel_time # homeassistant.components.google_travel_time
googlemaps==2.5.1 googlemaps==2.5.1
@ -1049,7 +1049,7 @@ minio==5.0.10
mitemp_bt==0.0.5 mitemp_bt==0.0.5
# homeassistant.components.motion_blinds # homeassistant.components.motion_blinds
motionblinds==0.5.10 motionblinds==0.5.12
# homeassistant.components.motioneye # homeassistant.components.motioneye
motioneye-client==0.3.12 motioneye-client==0.3.12
@ -1310,7 +1310,7 @@ pushbullet.py==0.11.0
pushover_complete==1.1.1 pushover_complete==1.1.1
# homeassistant.components.pvoutput # homeassistant.components.pvoutput
pvo==0.2.1 pvo==0.2.2
# homeassistant.components.rpi_gpio_pwm # homeassistant.components.rpi_gpio_pwm
pwmled==1.6.7 pwmled==1.6.7
@ -1331,7 +1331,7 @@ py-nightscout==1.2.2
py-schluter==0.1.7 py-schluter==0.1.7
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
py-synologydsm-api==1.0.5 py-synologydsm-api==1.0.6
# homeassistant.components.zabbix # homeassistant.components.zabbix
py-zabbix==1.1.7 py-zabbix==1.1.7
@ -1500,7 +1500,7 @@ pyeight==0.2.0
pyemby==1.8 pyemby==1.8
# homeassistant.components.envisalink # homeassistant.components.envisalink
pyenvisalink==4.3 pyenvisalink==4.4
# homeassistant.components.ephember # homeassistant.components.ephember
pyephember==0.3.1 pyephember==0.3.1
@ -1954,7 +1954,7 @@ python-mpd2==3.0.4
python-mystrom==1.1.2 python-mystrom==1.1.2
# homeassistant.components.nest # homeassistant.components.nest
python-nest==4.1.0 python-nest==4.2.0
# homeassistant.components.ozw # homeassistant.components.ozw
python-openzwave-mqtt[mqtt-client]==1.4.0 python-openzwave-mqtt[mqtt-client]==1.4.0
@ -2513,7 +2513,7 @@ xs1-api-client==3.0.0
yalesmartalarmclient==0.3.7 yalesmartalarmclient==0.3.7
# homeassistant.components.august # homeassistant.components.august
yalexs==1.1.20 yalexs==1.1.22
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.8 yeelight==0.7.8

View File

@ -91,7 +91,7 @@ aio_georss_gdacs==0.5
aioambient==2021.11.0 aioambient==2021.11.0
# homeassistant.components.aseko_pool_live # homeassistant.components.aseko_pool_live
aioaseko==0.0.1 aioaseko==0.0.2
# homeassistant.components.asuswrt # homeassistant.components.asuswrt
aioasuswrt==1.4.0 aioasuswrt==1.4.0
@ -119,7 +119,7 @@ aioeagle==1.1.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==10.8.1 aioesphomeapi==10.8.2
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@ -492,7 +492,7 @@ google-api-python-client==1.6.4
google-cloud-pubsub==2.9.0 google-cloud-pubsub==2.9.0
# homeassistant.components.nest # homeassistant.components.nest
google-nest-sdm==1.6.0 google-nest-sdm==1.7.1
# homeassistant.components.google_travel_time # homeassistant.components.google_travel_time
googlemaps==2.5.1 googlemaps==2.5.1
@ -655,7 +655,7 @@ millheater==0.9.0
minio==5.0.10 minio==5.0.10
# homeassistant.components.motion_blinds # homeassistant.components.motion_blinds
motionblinds==0.5.10 motionblinds==0.5.12
# homeassistant.components.motioneye # homeassistant.components.motioneye
motioneye-client==0.3.12 motioneye-client==0.3.12
@ -814,7 +814,7 @@ pure-python-adb[async]==0.3.0.dev0
pushbullet.py==0.11.0 pushbullet.py==0.11.0
# homeassistant.components.pvoutput # homeassistant.components.pvoutput
pvo==0.2.1 pvo==0.2.2
# homeassistant.components.canary # homeassistant.components.canary
py-canary==0.5.1 py-canary==0.5.1
@ -829,7 +829,7 @@ py-melissa-climate==2.1.4
py-nightscout==1.2.2 py-nightscout==1.2.2
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
py-synologydsm-api==1.0.5 py-synologydsm-api==1.0.6
# homeassistant.components.seventeentrack # homeassistant.components.seventeentrack
py17track==2021.12.2 py17track==2021.12.2
@ -1206,7 +1206,7 @@ python-kasa==0.4.1
python-miio==0.5.9.2 python-miio==0.5.9.2
# homeassistant.components.nest # homeassistant.components.nest
python-nest==4.1.0 python-nest==4.2.0
# homeassistant.components.ozw # homeassistant.components.ozw
python-openzwave-mqtt[mqtt-client]==1.4.0 python-openzwave-mqtt[mqtt-client]==1.4.0
@ -1541,7 +1541,7 @@ xmltodict==0.12.0
yalesmartalarmclient==0.3.7 yalesmartalarmclient==0.3.7
# homeassistant.components.august # homeassistant.components.august
yalexs==1.1.20 yalexs==1.1.22
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.7.8 yeelight==0.7.8

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = homeassistant name = homeassistant
version = 2022.2.5 version = 2022.2.6
author = The Home Assistant Authors author = The Home Assistant Authors
author_email = hello@home-assistant.io author_email = hello@home-assistant.io
license = Apache-2.0 license = Apache-2.0

View File

@ -61,3 +61,25 @@ async def test_sensor(
assert state.attributes.get(ATTR_ARCH) == "aargh" assert state.attributes.get(ATTR_ARCH) == "aargh"
assert state.attributes.get(ATTR_BRAND) == "Intel Ryzen 7" assert state.attributes.get(ATTR_BRAND) == "Intel Ryzen 7"
assert state.attributes.get(ATTR_HZ) == 3.6 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

View File

@ -1,6 +1,7 @@
"""The tests for the group cover platform.""" """The tests for the group cover platform."""
from datetime import timedelta from datetime import timedelta
import async_timeout
import pytest import pytest
from homeassistant.components.cover import ( 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_TILT).state == STATE_OPENING
assert hass.states.get(DEMO_COVER_POS).state == STATE_OPEN assert hass.states.get(DEMO_COVER_POS).state == STATE_OPEN
assert hass.states.get(COVER_GROUP).state == STATE_OPENING 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

View File

@ -1,6 +1,7 @@
"""The tests for the group fan platform.""" """The tests for the group fan platform."""
from unittest.mock import patch from unittest.mock import patch
import async_timeout
import pytest import pytest
from homeassistant import config as hass_config 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 assert percentage_full_fan_state.attributes[ATTR_DIRECTION] == DIRECTION_REVERSE
fan_group_state = hass.states.get(FAN_GROUP) fan_group_state = hass.states.get(FAN_GROUP)
assert fan_group_state.attributes[ATTR_DIRECTION] == DIRECTION_REVERSE 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

View File

@ -2,6 +2,7 @@
import unittest.mock import unittest.mock
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import async_timeout
import pytest import pytest
from homeassistant import config as hass_config 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): async def test_nested_group(hass):
"""Test nested light group.""" """Test nested light group."""
hass.states.async_set("light.kitchen", "on")
await async_setup_component( await async_setup_component(
hass, hass,
LIGHT_DOMAIN, LIGHT_DOMAIN,
{ {
LIGHT_DOMAIN: [ LIGHT_DOMAIN: [
{"platform": "demo"},
{ {
"platform": DOMAIN, "platform": DOMAIN,
"entities": ["light.bedroom_group"], "entities": ["light.bedroom_group"],
@ -1483,7 +1484,7 @@ async def test_nested_group(hass):
}, },
{ {
"platform": DOMAIN, "platform": DOMAIN,
"entities": ["light.kitchen", "light.bedroom"], "entities": ["light.bed_light", "light.kitchen_lights"],
"name": "Bedroom Group", "name": "Bedroom Group",
}, },
] ]
@ -1496,9 +1497,25 @@ async def test_nested_group(hass):
state = hass.states.get("light.bedroom_group") state = hass.states.get("light.bedroom_group")
assert state is not None assert state is not None
assert state.state == STATE_ON 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") state = hass.states.get("light.nested_group")
assert state is not None assert state is not None
assert state.state == STATE_ON assert state.state == STATE_ON
assert state.attributes.get(ATTR_ENTITY_ID) == ["light.bedroom_group"] 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

View File

@ -1,6 +1,7 @@
"""The tests for the Media group platform.""" """The tests for the Media group platform."""
from unittest.mock import patch from unittest.mock import patch
import async_timeout
import pytest import pytest
from homeassistant.components.group import DOMAIN 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): async def test_nested_group(hass):
"""Test nested media group.""" """Test nested media group."""
hass.states.async_set("media_player.player_1", "on")
await async_setup_component( await async_setup_component(
hass, hass,
MEDIA_DOMAIN, MEDIA_DOMAIN,
{ {
MEDIA_DOMAIN: [ MEDIA_DOMAIN: [
{"platform": "demo"},
{ {
"platform": DOMAIN, "platform": DOMAIN,
"entities": ["media_player.group_1"], "entities": ["media_player.group_1"],
@ -499,7 +500,7 @@ async def test_nested_group(hass):
}, },
{ {
"platform": DOMAIN, "platform": DOMAIN,
"entities": ["media_player.player_1", "media_player.player_2"], "entities": ["media_player.bedroom", "media_player.kitchen"],
"name": "Group 1", "name": "Group 1",
}, },
] ]
@ -511,13 +512,28 @@ async def test_nested_group(hass):
state = hass.states.get("media_player.group_1") state = hass.states.get("media_player.group_1")
assert state is not None assert state is not None
assert state.state == STATE_ON assert state.state == STATE_PLAYING
assert state.attributes.get(ATTR_ENTITY_ID) == [ assert state.attributes.get(ATTR_ENTITY_ID) == [
"media_player.player_1", "media_player.bedroom",
"media_player.player_2", "media_player.kitchen",
] ]
state = hass.states.get("media_player.nested_group") state = hass.states.get("media_player.nested_group")
assert state is not None 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"] 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

View File

@ -47,7 +47,7 @@ async def test_full_user_flow(
} }
assert len(mock_setup_entry.mock_calls) == 1 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( 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 result.get("step_id") == SOURCE_USER
assert "flow_id" in result 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( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
@ -83,9 +83,9 @@ async def test_full_flow_with_authentication_error(
assert "flow_id" in result2 assert "flow_id" in result2
assert len(mock_setup_entry.mock_calls) == 0 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( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
user_input={ 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_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( async def test_connection_error(
hass: HomeAssistant, mock_pvoutput_config_flow: MagicMock hass: HomeAssistant, mock_pvoutput_config_flow: MagicMock
) -> None: ) -> None:
"""Test API connection error.""" """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( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -123,7 +123,7 @@ async def test_connection_error(
assert result.get("type") == RESULT_TYPE_FORM assert result.get("type") == RESULT_TYPE_FORM
assert result.get("errors") == {"base": "cannot_connect"} 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( 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_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( 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_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( 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 result.get("step_id") == "reauth_confirm"
assert "flow_id" in result 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( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{CONF_API_KEY: "invalid_key"}, {CONF_API_KEY: "invalid_key"},
@ -256,9 +256,9 @@ async def test_reauth_with_authentication_error(
assert "flow_id" in result2 assert "flow_id" in result2
assert len(mock_setup_entry.mock_calls) == 0 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( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
user_input={CONF_API_KEY: "valid_key"}, 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_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( async def test_reauth_api_error(
@ -297,7 +297,7 @@ async def test_reauth_api_error(
assert result.get("step_id") == "reauth_confirm" assert result.get("step_id") == "reauth_confirm"
assert "flow_id" in result 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( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{CONF_API_KEY: "some_new_key"}, {CONF_API_KEY: "some_new_key"},

View File

@ -1,7 +1,11 @@
"""Tests for the PVOutput integration.""" """Tests for the PVOutput integration."""
from unittest.mock import MagicMock from unittest.mock import MagicMock
from pvo import PVOutputAuthenticationError, PVOutputConnectionError from pvo import (
PVOutputAuthenticationError,
PVOutputConnectionError,
PVOutputNoDataError,
)
import pytest import pytest
from homeassistant.components.pvoutput.const import CONF_SYSTEM_ID, DOMAIN 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 assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
@pytest.mark.parametrize("side_effect", [PVOutputConnectionError, PVOutputNoDataError])
async def test_config_entry_not_ready( async def test_config_entry_not_ready(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
mock_pvoutput: MagicMock, mock_pvoutput: MagicMock,
side_effect: Exception,
) -> None: ) -> None:
"""Test the PVOutput configuration entry not ready.""" """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) mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.config_entries.async_setup(mock_config_entry.entry_id)

View File

@ -1124,3 +1124,15 @@ async def test_deprecated_disabled_by_str(hass, registry, caplog):
assert entry.disabled_by is er.RegistryEntryDisabler.USER assert entry.disabled_by is er.RegistryEntryDisabler.USER
assert " str for entity registry disabled_by. This is deprecated " in caplog.text 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