diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index 1ec7dfabcfa..9cff1ff393d 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -123,7 +123,7 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): } _LOGGER.debug("update_hvac_params=%s", _params) try: - await self.coordinator.airzone.put_hvac(_params) + await self.coordinator.airzone.set_hvac_parameters(_params) except AirzoneError as error: raise HomeAssistantError( f"Failed to set zone {self.name}: {error}" diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index a6ea814fe9c..7a04b3a78b3 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.4.2"], + "requirements": ["aioairzone==0.4.3"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 6c1b35f70f8..5a7298dcbee 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -296,7 +296,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): _LOGGER.debug("Streaming %s via RAOP", media_id) await self.atv.stream.stream_file(media_id) - if self._is_feature_available(FeatureName.PlayUrl): + elif self._is_feature_available(FeatureName.PlayUrl): _LOGGER.debug("Playing %s via AirPlay", media_id) await self.atv.stream.play_url(media_id) else: diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index d0b7cfc4158..63f0693b01a 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Cast from a config entry.""" await home_assistant_cast.async_setup_ha_cast(hass, entry) hass.config_entries.async_setup_platforms(entry, PLATFORMS) - hass.data[DOMAIN] = {} + hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}} await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform) return True @@ -107,7 +107,7 @@ async def _register_cast_platform( or not hasattr(platform, "async_play_media") ): raise HomeAssistantError(f"Invalid cast platform {platform}") - hass.data[DOMAIN][integration_domain] = platform + hass.data[DOMAIN]["cast_platform"][integration_domain] = platform async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index bc0f1435f8b..485d2888a41 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -34,7 +34,7 @@ def discover_chromecast( _LOGGER.error("Discovered chromecast without uuid %s", info) return - info = info.fill_out_missing_chromecast_info() + info = info.fill_out_missing_chromecast_info(hass) _LOGGER.debug("Discovered new or updated chromecast %s", info) dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 5c6f0fee62a..dd98a2bc051 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -15,8 +15,11 @@ from pychromecast import dial from pychromecast.const import CAST_TYPE_GROUP from pychromecast.models import CastInfo +from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) _PLS_SECTION_PLAYLIST = "playlist" @@ -47,18 +50,50 @@ class ChromecastInfo: """Return the UUID.""" return self.cast_info.uuid - def fill_out_missing_chromecast_info(self) -> ChromecastInfo: + def fill_out_missing_chromecast_info(self, hass: HomeAssistant) -> ChromecastInfo: """Return a new ChromecastInfo object with missing attributes filled in. Uses blocking HTTP / HTTPS. """ cast_info = self.cast_info if self.cast_info.cast_type is None or self.cast_info.manufacturer is None: - # Manufacturer and cast type is not available in mDNS data, get it over http - cast_info = dial.get_cast_type( - cast_info, - zconf=ChromeCastZeroconf.get_zeroconf(), - ) + unknown_models = hass.data[DOMAIN]["unknown_models"] + if self.cast_info.model_name not in unknown_models: + # Manufacturer and cast type is not available in mDNS data, get it over http + cast_info = dial.get_cast_type( + cast_info, + zconf=ChromeCastZeroconf.get_zeroconf(), + ) + unknown_models[self.cast_info.model_name] = ( + cast_info.cast_type, + cast_info.manufacturer, + ) + + report_issue = ( + "create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + "+label%3A%22integration%3A+cast%22" + ) + + _LOGGER.info( + "Fetched cast details for unknown model '%s' manufacturer: '%s', type: '%s'. Please %s", + cast_info.model_name, + cast_info.manufacturer, + cast_info.cast_type, + report_issue, + ) + else: + cast_type, manufacturer = unknown_models[self.cast_info.model_name] + cast_info = CastInfo( + cast_info.services, + cast_info.uuid, + cast_info.model_name, + cast_info.friendly_name, + cast_info.host, + cast_info.port, + cast_type, + manufacturer, + ) if not self.is_audio_group or self.is_dynamic_group is not None: # We have all information, no need to check HTTP API. diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 389b837f200..cee46913937 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==12.0.0"], + "requirements": ["pychromecast==12.1.1"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index e63fedd3598..b64c3372c15 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -535,7 +535,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): """Generate root node.""" children = [] # Add media browsers - for platform in self.hass.data[CAST_DOMAIN].values(): + for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): children.extend( await platform.async_get_media_browser_root_object( self.hass, self._chromecast.cast_type @@ -587,7 +587,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): if media_content_id is None: return await self._async_root_payload(content_filter) - for platform in self.hass.data[CAST_DOMAIN].values(): + for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): browse_media = await platform.async_browse_media( self.hass, media_content_type, @@ -646,7 +646,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return # Try the cast platforms - for platform in self.hass.data[CAST_DOMAIN].values(): + for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): result = await platform.async_play_media( self.hass, self.entity_id, self._chromecast, media_type, media_id ) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 315d1b705df..213e8888e23 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.21.4"], + "requirements": ["numpy==1.21.6"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index b3cb68098e1..6087e07799d 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -7,6 +7,7 @@ from urllib.parse import urlparse from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN @@ -71,7 +72,11 @@ class DevoloDeviceEntity(Entity): def _generic_message(self, message: tuple) -> None: """Handle generic messages.""" - if len(message) == 3 and message[2] == "battery_level": + if ( + len(message) == 3 + and message[2] == "battery_level" + and self.device_class == SensorDeviceClass.BATTERY + ): self._value = message[1] elif len(message) == 3 and message[2] == "status": # Maybe the API wants to tell us, that the device went on- or offline. diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 50ddeb3bba7..9bb07157b54 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.21.4", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.21.6", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index bb8f94f3abe..3d9e07519a8 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -38,6 +38,7 @@ from .const import ( CONF_CA_CERTS, CONF_CERTFILE, CONF_KEYFILE, + CONFIG_URL, DOMAIN, LUTRON_CASETA_BUTTON_EVENT, MANUFACTURER, @@ -306,13 +307,15 @@ class LutronCasetaDevice(Entity): self._device = device self._smartbridge = bridge self._bridge_device = bridge_device + if "serial" not in self._device: + return info = DeviceInfo( identifiers={(DOMAIN, self.serial)}, manufacturer=MANUFACTURER, model=f"{device['model']} ({device['type']})", name=self.name, via_device=(DOMAIN, self._bridge_device["serial"]), - configuration_url="https://device-login.lutron.com", + configuration_url=CONFIG_URL, ) area, _ = _area_and_name_from_name(device["name"]) if area != UNASSIGNED_AREA: diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 788702f9353..f61e644a331 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -6,11 +6,14 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_SUGGESTED_AREA from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice -from .const import BRIDGE_DEVICE, BRIDGE_LEAP +from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice, _area_and_name_from_name +from .const import BRIDGE_DEVICE, BRIDGE_LEAP, CONFIG_URL, MANUFACTURER, UNASSIGNED_AREA async def async_setup_entry( @@ -39,6 +42,23 @@ async def async_setup_entry( class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): """Representation of a Lutron occupancy group.""" + def __init__(self, device, bridge, bridge_device): + """Init an occupancy sensor.""" + super().__init__(device, bridge, bridge_device) + info = DeviceInfo( + identifiers={(CASETA_DOMAIN, self.unique_id)}, + manufacturer=MANUFACTURER, + model="Lutron Occupancy", + name=self.name, + via_device=(CASETA_DOMAIN, self._bridge_device["serial"]), + configuration_url=CONFIG_URL, + entry_type=DeviceEntryType.SERVICE, + ) + area, _ = _area_and_name_from_name(device["name"]) + if area != UNASSIGNED_AREA: + info[ATTR_SUGGESTED_AREA] = area + self._attr_device_info = info + @property def device_class(self): """Flag supported features.""" @@ -65,16 +85,6 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): """Return a unique identifier.""" return f"occupancygroup_{self.device_id}" - @property - def device_info(self): - """Return the device info. - - Sensor entities are aggregated from one or more physical - sensors by each room. Therefore, there shouldn't be devices - related to any sensor entities. - """ - return None - @property def extra_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index 56a3821dd64..71d686ba2c8 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -35,3 +35,5 @@ CONF_SUBTYPE = "subtype" BRIDGE_TIMEOUT = 35 UNASSIGNED_AREA = "Unassigned" + +CONFIG_URL = "https://device-login.lutron.com" diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 17e8db9e473..8c719d588d8 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -146,13 +146,13 @@ async def async_setup_entry( if not coordinator.last_update_success: return - devices = coordinator.data + devices: dict[str, MeaterProbe] = coordinator.data entities = [] known_probes: set = hass.data[DOMAIN]["known_probes"] # Add entities for temperature probes which we've not yet seen for dev in devices: - if dev.id in known_probes: + if dev in known_probes: continue entities.extend( @@ -161,7 +161,7 @@ async def async_setup_entry( for sensor_description in SENSOR_TYPES ] ) - known_probes.add(dev.id) + known_probes.add(dev) async_add_entities(entities) diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 2b62500f23b..16231ef0b88 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -3,7 +3,7 @@ "name": "Nettigo Air Monitor", "documentation": "https://www.home-assistant.io/integrations/nam", "codeowners": ["@bieniu"], - "requirements": ["nettigo-air-monitor==1.2.2"], + "requirements": ["nettigo-air-monitor==1.2.3"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 9c1f51c4933..504b83bdaf9 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.21.4", "opencv-python-headless==4.5.2.54"], + "requirements": ["numpy==1.21.6", "opencv-python-headless==4.5.2.54"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index ea8b8fe59cb..e75d7117d73 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -9,7 +9,7 @@ from homeassistant.components import cloud from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from .const import CONF_CLOUDHOOK_URL, CONF_MANUAL_RUN_MINS, CONF_WEBHOOK_ID, DOMAIN @@ -73,6 +73,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Get the API user try: await person.async_setup(hass) + except ConfigEntryAuthFailed as error: + # Reauth is not yet implemented + _LOGGER.error("Authentication failed: %s", error) + return False except ConnectTimeout as error: _LOGGER.error("Could not reach the Rachio API: %s", error) raise ConfigEntryNotReady from error diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index ff7c0535295..911049883d9 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import ServiceCall +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from .const import ( @@ -125,12 +126,18 @@ class RachioPerson: rachio = self.rachio response = rachio.person.info() - assert int(response[0][KEY_STATUS]) == HTTPStatus.OK, "API key error" + if is_invalid_auth_code(int(response[0][KEY_STATUS])): + raise ConfigEntryAuthFailed(f"API key error: {response}") + if int(response[0][KEY_STATUS]) != HTTPStatus.OK: + raise ConfigEntryNotReady(f"API Error: {response}") self._id = response[1][KEY_ID] # Use user ID to get user data data = rachio.person.get(self._id) - assert int(data[0][KEY_STATUS]) == HTTPStatus.OK, "User ID error" + if is_invalid_auth_code(int(data[0][KEY_STATUS])): + raise ConfigEntryAuthFailed(f"User ID error: {data}") + if int(data[0][KEY_STATUS]) != HTTPStatus.OK: + raise ConfigEntryNotReady(f"API Error: {data}") self.username = data[1][KEY_USERNAME] devices = data[1][KEY_DEVICES] for controller in devices: @@ -297,3 +304,11 @@ class RachioIro: """Resume paused watering on this controller.""" self.rachio.device.resume_zone_run(self.controller_id) _LOGGER.debug("Resuming watering on %s", self) + + +def is_invalid_auth_code(http_status_code): + """HTTP status codes that mean invalid auth.""" + if http_status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): + return True + + return False diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 0702446d217..f112893b5e1 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -3,8 +3,6 @@ "name": "SABnzbd", "documentation": "https://www.home-assistant.io/integrations/sabnzbd", "requirements": ["pysabnzbd==1.1.1"], - "dependencies": ["configurator"], - "after_dependencies": ["discovery"], "codeowners": ["@shaiu"], "iot_class": "local_polling", "config_flow": true, diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 1d661d90848..dee80945e9d 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -14,8 +14,10 @@ from . import DOMAIN, SIGNAL_SABNZBD_UPDATED from ...config_entries import ConfigEntry from ...const import DATA_GIGABYTES, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND from ...core import HomeAssistant +from ...helpers.device_registry import DeviceEntryType +from ...helpers.entity import DeviceInfo from ...helpers.entity_platform import AddEntitiesCallback -from .const import KEY_API_DATA, KEY_NAME +from .const import DEFAULT_NAME, KEY_API_DATA, KEY_NAME @dataclass @@ -127,9 +129,16 @@ class SabnzbdSensor(SensorEntity): self, sabnzbd_api_data, client_name, description: SabnzbdSensorEntityDescription ): """Initialize the sensor.""" + unique_id = description.key + self._attr_unique_id = unique_id self.entity_description = description self._sabnzbd_api = sabnzbd_api_data self._attr_name = f"{client_name} {description.name}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, DOMAIN)}, + name=DEFAULT_NAME, + ) async def async_added_to_hass(self): """Call when entity about to be added to hass.""" diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index dae4033ad4c..a7b8f7d1aec 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -30,7 +30,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.typing import ConfigType -from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info +from .bridge import ( + SamsungTVBridge, + async_get_device_info, + mac_from_device_info, + model_requires_encryption, +) from .const import ( CONF_ON_ACTION, CONF_SESSION_ID, @@ -214,11 +219,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -def _model_requires_encryption(model: str | None) -> bool: - """H and J models need pairing with PIN.""" - return model is not None and len(model) > 4 and model[4] in ("H", "J") - - async def _async_create_bridge_with_updated_data( hass: HomeAssistant, entry: ConfigEntry ) -> SamsungTVBridge: @@ -279,7 +279,7 @@ async def _async_create_bridge_with_updated_data( LOGGER.info("Updated model to %s for %s", model, host) updated_data[CONF_MODEL] = model - if _model_requires_encryption(model) and method != METHOD_ENCRYPTED_WEBSOCKET: + if model_requires_encryption(model) and method != METHOD_ENCRYPTED_WEBSOCKET: LOGGER.info( "Detected model %s for %s. Some televisions from H and J series use " "an encrypted protocol but you are using %s which may not be supported", diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 52ab86337dd..c3201a493eb 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -85,6 +85,11 @@ def mac_from_device_info(info: dict[str, Any]) -> str | None: return None +def model_requires_encryption(model: str | None) -> bool: + """H and J models need pairing with PIN.""" + return model is not None and len(model) > 4 and model[4] in ("H", "J") + + async def async_get_device_info( hass: HomeAssistant, host: str, @@ -99,17 +104,19 @@ async def async_get_device_info( port, info, ) - encrypted_bridge = SamsungTVEncryptedBridge( - hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT - ) - result = await encrypted_bridge.async_try_connect() - if result != RESULT_CANNOT_CONNECT: - return ( - result, - ENCRYPTED_WEBSOCKET_PORT, - METHOD_ENCRYPTED_WEBSOCKET, - info, + # Check the encrypted port if the model requires encryption + if model_requires_encryption(info.get("device", {}).get("modelName")): + encrypted_bridge = SamsungTVEncryptedBridge( + hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT ) + result = await encrypted_bridge.async_try_connect() + if result != RESULT_CANNOT_CONNECT: + return ( + result, + ENCRYPTED_WEBSOCKET_PORT, + METHOD_ENCRYPTED_WEBSOCKET, + info, + ) return RESULT_SUCCESS, port, METHOD_WEBSOCKET, info # Try legacy port diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 5f1ac406b70..0f53dd61cb4 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.21.4", + "numpy==1.21.6", "pillow==9.1.0" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 831e97aed3d..aaae8f7cc54 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.21.4"], + "requirements": ["numpy==1.21.6"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/const.py b/homeassistant/const.py index 5c7b28e1156..c2cb8119602 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 = 5 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __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_registry.py b/homeassistant/helpers/entity_registry.py index b4dd0820d8c..64ab0323f6c 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -711,6 +711,10 @@ class EntityRegistry: if not valid_entity_id(entity["entity_id"]): continue + # We removed this in 2022.5. Remove this check in 2023.1. + if entity["entity_category"] == "system": + entity["entity_category"] = None + entities[entity["entity_id"]] = RegistryEntry( area_id=entity["area_id"], capabilities=entity["capabilities"], diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index eecc66c8332..afe74a4d7af 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -5,11 +5,13 @@ from collections.abc import Callable, Sequence from typing import Any, TypedDict, cast import voluptuous as vol +import yaml from homeassistant.backports.enum import StrEnum from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.util import decorator +from homeassistant.util.yaml.dumper import represent_odict from . import config_validation as cv @@ -71,7 +73,11 @@ class Selector: def serialize(self) -> Any: """Serialize Selector for voluptuous_serialize.""" - return {"selector": {self.selector_type: self.config}} + return {"selector": {self.selector_type: self.serialize_config()}} + + def serialize_config(self) -> Any: + """Serialize config.""" + return self.config SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema( @@ -623,6 +629,13 @@ class NumberSelector(Selector): """Instantiate a selector.""" super().__init__(config) + def serialize_config(self) -> Any: + """Serialize the selector config.""" + return { + **self.config, + "mode": self.config["mode"].value, + } + def __call__(self, data: Any) -> float: """Validate the passed selection.""" value: float = vol.Coerce(float)(data) @@ -881,3 +894,11 @@ class TimeSelector(Selector): """Validate the passed selection.""" cv.time(data) return cast(str, data) + + +yaml.SafeDumper.add_representer( + Selector, + lambda dumper, value: represent_odict( + dumper, "tag:yaml.org,2002:map", value.serialize() + ), +) diff --git a/requirements_all.txt b/requirements_all.txt index 787e4546b0b..b6d512d97be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -110,7 +110,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.2 +aioairzone==0.4.3 # homeassistant.components.ambient_station aioambient==2021.11.0 @@ -1065,7 +1065,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.2.2 +nettigo-air-monitor==1.2.3 # homeassistant.components.neurio_energy neurio==0.3.1 @@ -1111,7 +1111,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.4 +numpy==1.21.6 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -1399,7 +1399,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==12.0.0 +pychromecast==12.1.1 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f9f4b59b0e..6f93d8c5e82 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.2 +aioairzone==0.4.3 # homeassistant.components.ambient_station aioambient==2021.11.0 @@ -727,7 +727,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.2.2 +nettigo-air-monitor==1.2.3 # homeassistant.components.nexia nexia==0.9.13 @@ -755,7 +755,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.4 +numpy==1.21.6 # homeassistant.components.google oauth2client==4.1.3 @@ -938,7 +938,7 @@ pybotvac==0.0.23 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==12.0.0 +pychromecast==12.1.1 # homeassistant.components.climacell pyclimacell==0.18.2 diff --git a/setup.cfg b/setup.cfg index beab88224e5..04a68db8ca6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.0 +version = 2022.5.1 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index 2128d2818e7..dcb493351ba 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -147,7 +147,7 @@ async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None: ] } with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ): await hass.services.async_call( @@ -172,7 +172,7 @@ async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None: ] } with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ): await hass.services.async_call( @@ -204,7 +204,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: ] } with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ): await hass.services.async_call( @@ -230,7 +230,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: ] } with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK_2, ): await hass.services.async_call( @@ -263,7 +263,7 @@ async def test_airzone_climate_set_hvac_slave_error(hass: HomeAssistant) -> None await async_init_integration(hass) with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ), pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -296,7 +296,7 @@ async def test_airzone_climate_set_temp(hass: HomeAssistant) -> None: await async_init_integration(hass) with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ): await hass.services.async_call( diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 0e1e66405e6..806cdb2cb8d 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -198,7 +198,7 @@ async def test_fetch_blueprint_from_github_url(hass, aioclient_mock, url): assert imported_blueprint.blueprint.domain == "automation" assert imported_blueprint.blueprint.inputs == { "service_to_call": None, - "trigger_event": None, + "trigger_event": {"selector": {"text": {}}}, } assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.blueprint.metadata["source_url"] == url diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 51b8184354a..40f24d98016 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -30,7 +30,10 @@ async def test_list_blueprints(hass, hass_ws_client): "test_event_service.yaml": { "metadata": { "domain": "automation", - "input": {"service_to_call": None, "trigger_event": None}, + "input": { + "service_to_call": None, + "trigger_event": {"selector": {"text": {}}}, + }, "name": "Call service based on event", }, }, @@ -89,7 +92,10 @@ async def test_import_blueprint(hass, aioclient_mock, hass_ws_client): "blueprint": { "metadata": { "domain": "automation", - "input": {"service_to_call": None, "trigger_event": None}, + "input": { + "service_to_call": None, + "trigger_event": {"selector": {"text": {}}}, + }, "name": "Call service based on event", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", }, @@ -123,7 +129,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["success"] assert write_mock.mock_calls assert write_mock.call_args[0] == ( - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", ) diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index 3b96f378906..52152b4a718 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -14,6 +14,13 @@ def get_multizone_status_mock(): return mock +@pytest.fixture() +def get_cast_type_mock(): + """Mock pychromecast dial.""" + mock = MagicMock(spec_set=pychromecast.dial.get_cast_type) + return mock + + @pytest.fixture() def castbrowser_mock(): """Mock pychromecast CastBrowser.""" @@ -43,6 +50,7 @@ def cast_mock( mz_mock, quick_play_mock, castbrowser_mock, + get_cast_type_mock, get_chromecast_mock, get_multizone_status_mock, ): @@ -52,6 +60,9 @@ def cast_mock( with patch( "homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser", castbrowser_mock, + ), patch( + "homeassistant.components.cast.helpers.dial.get_cast_type", + get_cast_type_mock, ), patch( "homeassistant.components.cast.helpers.dial.get_multizone_status", get_multizone_status_mock, diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 88cf281c8a5..e4df84f6443 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -64,6 +64,8 @@ FAKE_MDNS_SERVICE = pychromecast.discovery.ServiceInfo( pychromecast.const.SERVICE_TYPE_MDNS, "the-service" ) +UNDEFINED = object() + def get_fake_chromecast(info: ChromecastInfo): """Generate a Fake Chromecast object with the specified arguments.""" @@ -74,7 +76,14 @@ def get_fake_chromecast(info: ChromecastInfo): def get_fake_chromecast_info( - host="192.168.178.42", port=8009, service=None, uuid: UUID | None = FakeUUID + *, + host="192.168.178.42", + port=8009, + service=None, + uuid: UUID | None = FakeUUID, + cast_type=UNDEFINED, + manufacturer=UNDEFINED, + model_name=UNDEFINED, ): """Generate a Fake ChromecastInfo with the specified arguments.""" @@ -82,16 +91,22 @@ def get_fake_chromecast_info( service = pychromecast.discovery.ServiceInfo( pychromecast.const.SERVICE_TYPE_HOST, (host, port) ) + if cast_type is UNDEFINED: + cast_type = CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST + if manufacturer is UNDEFINED: + manufacturer = "Nabu Casa" + if model_name is UNDEFINED: + model_name = "Chromecast" return ChromecastInfo( cast_info=pychromecast.models.CastInfo( services={service}, uuid=uuid, - model_name="Chromecast", + model_name=model_name, friendly_name="Speaker", host=host, port=port, - cast_type=CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST, - manufacturer="Nabu Casa", + cast_type=cast_type, + manufacturer=manufacturer, ) ) @@ -342,6 +357,92 @@ async def test_internal_discovery_callback_fill_out_group( get_multizone_status_mock.assert_called_once() +async def test_internal_discovery_callback_fill_out_cast_type_manufacturer( + hass, get_cast_type_mock, caplog +): + """Test internal discovery automatically filling out information.""" + discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info( + host="host1", + port=8009, + service=FAKE_MDNS_SERVICE, + cast_type=None, + manufacturer=None, + ) + info2 = get_fake_chromecast_info( + host="host1", + port=8009, + service=FAKE_MDNS_SERVICE, + cast_type=None, + manufacturer=None, + model_name="Model 101", + ) + zconf = get_fake_zconf(host="host1", port=8009) + full_info = attr.evolve( + info, + cast_info=pychromecast.discovery.CastInfo( + services=info.cast_info.services, + uuid=FakeUUID, + model_name="Chromecast", + friendly_name="Speaker", + host=info.cast_info.host, + port=info.cast_info.port, + cast_type="audio", + manufacturer="TrollTech", + ), + is_dynamic_group=None, + ) + full_info2 = attr.evolve( + info2, + cast_info=pychromecast.discovery.CastInfo( + services=info.cast_info.services, + uuid=FakeUUID, + model_name="Model 101", + friendly_name="Speaker", + host=info.cast_info.host, + port=info.cast_info.port, + cast_type="cast", + manufacturer="Cyberdyne Systems", + ), + is_dynamic_group=None, + ) + + get_cast_type_mock.assert_not_called() + get_cast_type_mock.return_value = full_info.cast_info + + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ): + signal = MagicMock() + + async_dispatcher_connect(hass, "cast_discovered", signal) + discover_cast(FAKE_MDNS_SERVICE, info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + get_cast_type_mock.assert_called_once() + assert get_cast_type_mock.call_count == 1 + discover = signal.mock_calls[0][1][0] + assert discover == full_info + assert "Fetched cast details for unknown model 'Chromecast'" in caplog.text + + # Call again, the model name should be fetched from cache + discover_cast(FAKE_MDNS_SERVICE, info) + await hass.async_block_till_done() + assert get_cast_type_mock.call_count == 1 # No additional calls + discover = signal.mock_calls[1][1][0] + assert discover == full_info + + # Call for another model, need to call HTTP again + get_cast_type_mock.return_value = full_info2.cast_info + discover_cast(FAKE_MDNS_SERVICE, info2) + await hass.async_block_till_done() + assert get_cast_type_mock.call_count == 2 + discover = signal.mock_calls[2][1][0] + assert discover == full_info2 + + async def test_stop_discovery_called_on_stop(hass, castbrowser_mock): """Test pychromecast.stop_discovery called on shutdown.""" # start_discovery should be called with empty config diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index d7f8ed0d1a1..764022f3501 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -22,7 +22,7 @@ from samsungtvws.remote import ChannelEmitCommand from homeassistant.components.samsungtv.const import WEBSOCKET_SSL_PORT import homeassistant.util.dt as dt_util -from .const import SAMPLE_DEVICE_INFO_WIFI +from .const import SAMPLE_DEVICE_INFO_UE48JU6400, SAMPLE_DEVICE_INFO_WIFI @pytest.fixture(autouse=True) @@ -177,7 +177,7 @@ def rest_api_fixture_non_ssl_only() -> Mock: """Mock rest_device_info to fail for ssl and work for non-ssl.""" if self.port == WEBSOCKET_SSL_PORT: raise ResponseError - return SAMPLE_DEVICE_INFO_WIFI + return SAMPLE_DEVICE_INFO_UE48JU6400 with patch( "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index d2a9d10caf2..40397a68d7d 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -340,16 +340,16 @@ async def test_user_encrypted_websocket( ) assert result4["type"] == "create_entry" - assert result4["title"] == "Living Room (82GXARRS)" + assert result4["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result4["data"][CONF_HOST] == "fake_host" - assert result4["data"][CONF_NAME] == "Living Room" + assert result4["data"][CONF_NAME] == "TV-UE48JU6470" assert result4["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result4["data"][CONF_MANUFACTURER] == "Samsung" - assert result4["data"][CONF_MODEL] == "82GXARRS" + assert result4["data"][CONF_MODEL] == "UE48JU6400" assert result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] is None assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" assert result4["data"][CONF_SESSION_ID] == "1" - assert result4["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + assert result4["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" @pytest.mark.usefixtures("rest_api_failing") @@ -714,19 +714,19 @@ async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_l ) assert result4["type"] == "create_entry" - assert result4["title"] == "Living Room (82GXARRS)" + assert result4["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result4["data"][CONF_HOST] == "fake_host" - assert result4["data"][CONF_NAME] == "Living Room" + assert result4["data"][CONF_NAME] == "TV-UE48JU6470" assert result4["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result4["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" - assert result4["data"][CONF_MODEL] == "82GXARRS" + assert result4["data"][CONF_MODEL] == "UE48JU6400" assert ( result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] == "https://fake_host:12345/test" ) assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" assert result4["data"][CONF_SESSION_ID] == "1" - assert result4["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + assert result4["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" @pytest.mark.usefixtures("rest_api_non_ssl_only") @@ -1036,13 +1036,13 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: result["flow_id"], user_input="whatever" ) assert result["type"] == "create_entry" - assert result["title"] == "Living Room (82GXARRS)" + assert result["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] == "Living Room" + assert result["data"][CONF_NAME] == "TV-UE48JU6470" assert result["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result["data"][CONF_MANUFACTURER] == "Samsung" - assert result["data"][CONF_MODEL] == "82GXARRS" - assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + assert result["data"][CONF_MODEL] == "UE48JU6400" + assert result["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 64c838e6c02..a5d2223a3d2 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -11,7 +11,7 @@ import pytest import voluptuous as vol import homeassistant -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import config_validation as cv, selector, template def test_boolean(): @@ -720,6 +720,17 @@ def test_string_in_serializer(): } +def test_selector_in_serializer(): + """Test selector with custom_serializer.""" + assert cv.custom_serializer(selector.selector({"text": {}})) == { + "selector": { + "text": { + "multiline": False, + } + } + } + + def test_positive_time_period_dict_in_serializer(): """Test positive_time_period_dict with custom_serializer.""" assert cv.custom_serializer(cv.positive_time_period_dict) == { diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 41ac7412d9c..21d29736bd0 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -297,6 +297,12 @@ async def test_loading_extra_values(hass, hass_storage): "unique_id": "invalid-hass", "disabled_by": er.RegistryEntryDisabler.HASS, }, + { + "entity_id": "test.system_entity", + "platform": "super_platform", + "unique_id": "system-entity", + "entity_category": "system", + }, ] }, } @@ -304,7 +310,7 @@ async def test_loading_extra_values(hass, hass_storage): await er.async_load(hass) registry = er.async_get(hass) - assert len(registry.entities) == 4 + assert len(registry.entities) == 5 entry_with_name = registry.async_get_or_create( "test", "super_platform", "with-name" @@ -327,6 +333,11 @@ async def test_loading_extra_values(hass, hass_storage): assert entry_disabled_user.disabled assert entry_disabled_user.disabled_by is er.RegistryEntryDisabler.USER + entry_system_category = registry.async_get_or_create( + "test", "system_entity", "system-entity" + ) + assert entry_system_category.entity_category is None + def test_async_get_entity_id(registry): """Test that entity_id is returned.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index cb0ad95eb6b..8c94e3d3c56 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -2,7 +2,8 @@ import pytest import voluptuous as vol -from homeassistant.helpers import config_validation as cv, selector +from homeassistant.helpers import selector +from homeassistant.util import yaml FAKE_UUID = "a266a680b608c32770e6c45bfe6b8411" @@ -48,10 +49,12 @@ def _test_selector( converter = default_converter # Validate selector configuration - selector.validate_selector({selector_type: schema}) + config = {selector_type: schema} + selector.validate_selector(config) + selector_instance = selector.selector(config) # Use selector in schema and validate - vol_schema = vol.Schema({"selection": selector.selector({selector_type: schema})}) + vol_schema = vol.Schema({"selection": selector_instance}) for selection in valid_selections: assert vol_schema({"selection": selection}) == { "selection": converter(selection) @@ -62,9 +65,12 @@ def _test_selector( # Serialize selector selector_instance = selector.selector({selector_type: schema}) - assert cv.custom_serializer(selector_instance) == { - "selector": {selector_type: selector_instance.config} - } + assert ( + selector.selector(selector_instance.serialize()["selector"]).config + == selector_instance.config + ) + # Test serialized selector can be dumped to YAML + yaml.dump(selector_instance.serialize()) @pytest.mark.parametrize( diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml index ab067b004ac..648cef39b96 100644 --- a/tests/testing_config/blueprints/automation/test_event_service.yaml +++ b/tests/testing_config/blueprints/automation/test_event_service.yaml @@ -3,6 +3,8 @@ blueprint: domain: automation input: trigger_event: + selector: + text: service_to_call: trigger: platform: event