Merge pull request #71376 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-05-05 16:05:29 -07:00 committed by GitHub
commit 97c7d40d8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 370 additions and 109 deletions

View File

@ -123,7 +123,7 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
} }
_LOGGER.debug("update_hvac_params=%s", _params) _LOGGER.debug("update_hvac_params=%s", _params)
try: try:
await self.coordinator.airzone.put_hvac(_params) await self.coordinator.airzone.set_hvac_parameters(_params)
except AirzoneError as error: except AirzoneError as error:
raise HomeAssistantError( raise HomeAssistantError(
f"Failed to set zone {self.name}: {error}" f"Failed to set zone {self.name}: {error}"

View File

@ -3,7 +3,7 @@
"name": "Airzone", "name": "Airzone",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"requirements": ["aioairzone==0.4.2"], "requirements": ["aioairzone==0.4.3"],
"codeowners": ["@Noltari"], "codeowners": ["@Noltari"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"] "loggers": ["aioairzone"]

View File

@ -296,7 +296,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
_LOGGER.debug("Streaming %s via RAOP", media_id) _LOGGER.debug("Streaming %s via RAOP", media_id)
await self.atv.stream.stream_file(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) _LOGGER.debug("Playing %s via AirPlay", media_id)
await self.atv.stream.play_url(media_id) await self.atv.stream.play_url(media_id)
else: else:

View File

@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Cast from a config entry.""" """Set up Cast from a config entry."""
await home_assistant_cast.async_setup_ha_cast(hass, entry) await home_assistant_cast.async_setup_ha_cast(hass, entry)
hass.config_entries.async_setup_platforms(entry, PLATFORMS) 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) await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform)
return True return True
@ -107,7 +107,7 @@ async def _register_cast_platform(
or not hasattr(platform, "async_play_media") or not hasattr(platform, "async_play_media")
): ):
raise HomeAssistantError(f"Invalid cast platform {platform}") 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: async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:

View File

@ -34,7 +34,7 @@ def discover_chromecast(
_LOGGER.error("Discovered chromecast without uuid %s", info) _LOGGER.error("Discovered chromecast without uuid %s", info)
return 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) _LOGGER.debug("Discovered new or updated chromecast %s", info)
dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info)

View File

@ -15,8 +15,11 @@ from pychromecast import dial
from pychromecast.const import CAST_TYPE_GROUP from pychromecast.const import CAST_TYPE_GROUP
from pychromecast.models import CastInfo from pychromecast.models import CastInfo
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_PLS_SECTION_PLAYLIST = "playlist" _PLS_SECTION_PLAYLIST = "playlist"
@ -47,18 +50,50 @@ class ChromecastInfo:
"""Return the UUID.""" """Return the UUID."""
return self.cast_info.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. """Return a new ChromecastInfo object with missing attributes filled in.
Uses blocking HTTP / HTTPS. Uses blocking HTTP / HTTPS.
""" """
cast_info = self.cast_info cast_info = self.cast_info
if self.cast_info.cast_type is None or self.cast_info.manufacturer is None: 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 unknown_models = hass.data[DOMAIN]["unknown_models"]
cast_info = dial.get_cast_type( if self.cast_info.model_name not in unknown_models:
cast_info, # Manufacturer and cast type is not available in mDNS data, get it over http
zconf=ChromeCastZeroconf.get_zeroconf(), 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: if not self.is_audio_group or self.is_dynamic_group is not None:
# We have all information, no need to check HTTP API. # We have all information, no need to check HTTP API.

View File

@ -3,7 +3,7 @@
"name": "Google Cast", "name": "Google Cast",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast", "documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==12.0.0"], "requirements": ["pychromecast==12.1.1"],
"after_dependencies": [ "after_dependencies": [
"cloud", "cloud",
"http", "http",

View File

@ -535,7 +535,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
"""Generate root node.""" """Generate root node."""
children = [] children = []
# Add media browsers # 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( children.extend(
await platform.async_get_media_browser_root_object( await platform.async_get_media_browser_root_object(
self.hass, self._chromecast.cast_type self.hass, self._chromecast.cast_type
@ -587,7 +587,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
if media_content_id is None: if media_content_id is None:
return await self._async_root_payload(content_filter) 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( browse_media = await platform.async_browse_media(
self.hass, self.hass,
media_content_type, media_content_type,
@ -646,7 +646,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return return
# Try the cast platforms # 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( result = await platform.async_play_media(
self.hass, self.entity_id, self._chromecast, media_type, media_id self.hass, self.entity_id, self._chromecast, media_type, media_id
) )

View File

@ -2,7 +2,7 @@
"domain": "compensation", "domain": "compensation",
"name": "Compensation", "name": "Compensation",
"documentation": "https://www.home-assistant.io/integrations/compensation", "documentation": "https://www.home-assistant.io/integrations/compensation",
"requirements": ["numpy==1.21.4"], "requirements": ["numpy==1.21.6"],
"codeowners": ["@Petro31"], "codeowners": ["@Petro31"],
"iot_class": "calculated" "iot_class": "calculated"
} }

View File

@ -7,6 +7,7 @@ from urllib.parse import urlparse
from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.devices.zwave import Zwave
from devolo_home_control_api.homecontrol import HomeControl from devolo_home_control_api.homecontrol import HomeControl
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import DOMAIN from .const import DOMAIN
@ -71,7 +72,11 @@ class DevoloDeviceEntity(Entity):
def _generic_message(self, message: tuple) -> None: def _generic_message(self, message: tuple) -> None:
"""Handle generic messages.""" """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] self._value = message[1]
elif len(message) == 3 and message[2] == "status": elif len(message) == 3 and message[2] == "status":
# Maybe the API wants to tell us, that the device went on- or offline. # Maybe the API wants to tell us, that the device went on- or offline.

View File

@ -3,7 +3,7 @@
"name": "IQVIA", "name": "IQVIA",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/iqvia", "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"], "codeowners": ["@bachya"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyiqvia"] "loggers": ["pyiqvia"]

View File

@ -38,6 +38,7 @@ from .const import (
CONF_CA_CERTS, CONF_CA_CERTS,
CONF_CERTFILE, CONF_CERTFILE,
CONF_KEYFILE, CONF_KEYFILE,
CONFIG_URL,
DOMAIN, DOMAIN,
LUTRON_CASETA_BUTTON_EVENT, LUTRON_CASETA_BUTTON_EVENT,
MANUFACTURER, MANUFACTURER,
@ -306,13 +307,15 @@ class LutronCasetaDevice(Entity):
self._device = device self._device = device
self._smartbridge = bridge self._smartbridge = bridge
self._bridge_device = bridge_device self._bridge_device = bridge_device
if "serial" not in self._device:
return
info = DeviceInfo( info = DeviceInfo(
identifiers={(DOMAIN, self.serial)}, identifiers={(DOMAIN, self.serial)},
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
model=f"{device['model']} ({device['type']})", model=f"{device['model']} ({device['type']})",
name=self.name, name=self.name,
via_device=(DOMAIN, self._bridge_device["serial"]), 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"]) area, _ = _area_and_name_from_name(device["name"])
if area != UNASSIGNED_AREA: if area != UNASSIGNED_AREA:

View File

@ -6,11 +6,14 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_SUGGESTED_AREA
from homeassistant.core import HomeAssistant 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 homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice, _area_and_name_from_name
from .const import BRIDGE_DEVICE, BRIDGE_LEAP from .const import BRIDGE_DEVICE, BRIDGE_LEAP, CONFIG_URL, MANUFACTURER, UNASSIGNED_AREA
async def async_setup_entry( async def async_setup_entry(
@ -39,6 +42,23 @@ async def async_setup_entry(
class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity):
"""Representation of a Lutron occupancy group.""" """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 @property
def device_class(self): def device_class(self):
"""Flag supported features.""" """Flag supported features."""
@ -65,16 +85,6 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity):
"""Return a unique identifier.""" """Return a unique identifier."""
return f"occupancygroup_{self.device_id}" 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 @property
def extra_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""

View File

@ -35,3 +35,5 @@ CONF_SUBTYPE = "subtype"
BRIDGE_TIMEOUT = 35 BRIDGE_TIMEOUT = 35
UNASSIGNED_AREA = "Unassigned" UNASSIGNED_AREA = "Unassigned"
CONFIG_URL = "https://device-login.lutron.com"

View File

@ -146,13 +146,13 @@ async def async_setup_entry(
if not coordinator.last_update_success: if not coordinator.last_update_success:
return return
devices = coordinator.data devices: dict[str, MeaterProbe] = coordinator.data
entities = [] entities = []
known_probes: set = hass.data[DOMAIN]["known_probes"] known_probes: set = hass.data[DOMAIN]["known_probes"]
# Add entities for temperature probes which we've not yet seen # Add entities for temperature probes which we've not yet seen
for dev in devices: for dev in devices:
if dev.id in known_probes: if dev in known_probes:
continue continue
entities.extend( entities.extend(
@ -161,7 +161,7 @@ async def async_setup_entry(
for sensor_description in SENSOR_TYPES for sensor_description in SENSOR_TYPES
] ]
) )
known_probes.add(dev.id) known_probes.add(dev)
async_add_entities(entities) async_add_entities(entities)

View File

@ -3,7 +3,7 @@
"name": "Nettigo Air Monitor", "name": "Nettigo Air Monitor",
"documentation": "https://www.home-assistant.io/integrations/nam", "documentation": "https://www.home-assistant.io/integrations/nam",
"codeowners": ["@bieniu"], "codeowners": ["@bieniu"],
"requirements": ["nettigo-air-monitor==1.2.2"], "requirements": ["nettigo-air-monitor==1.2.3"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_http._tcp.local.", "type": "_http._tcp.local.",

View File

@ -2,7 +2,7 @@
"domain": "opencv", "domain": "opencv",
"name": "OpenCV", "name": "OpenCV",
"documentation": "https://www.home-assistant.io/integrations/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": [], "codeowners": [],
"iot_class": "local_push" "iot_class": "local_push"
} }

View File

@ -9,7 +9,7 @@ from homeassistant.components import cloud
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant 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 homeassistant.helpers import config_validation as cv
from .const import CONF_CLOUDHOOK_URL, CONF_MANUAL_RUN_MINS, CONF_WEBHOOK_ID, DOMAIN 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 # Get the API user
try: try:
await person.async_setup(hass) 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: except ConnectTimeout as error:
_LOGGER.error("Could not reach the Rachio API: %s", error) _LOGGER.error("Could not reach the Rachio API: %s", error)
raise ConfigEntryNotReady from error raise ConfigEntryNotReady from error

View File

@ -8,6 +8,7 @@ import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import ServiceCall from homeassistant.core import ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from .const import ( from .const import (
@ -125,12 +126,18 @@ class RachioPerson:
rachio = self.rachio rachio = self.rachio
response = rachio.person.info() 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] self._id = response[1][KEY_ID]
# Use user ID to get user data # Use user ID to get user data
data = rachio.person.get(self._id) 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] self.username = data[1][KEY_USERNAME]
devices = data[1][KEY_DEVICES] devices = data[1][KEY_DEVICES]
for controller in devices: for controller in devices:
@ -297,3 +304,11 @@ class RachioIro:
"""Resume paused watering on this controller.""" """Resume paused watering on this controller."""
self.rachio.device.resume_zone_run(self.controller_id) self.rachio.device.resume_zone_run(self.controller_id)
_LOGGER.debug("Resuming watering on %s", self) _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

View File

@ -3,8 +3,6 @@
"name": "SABnzbd", "name": "SABnzbd",
"documentation": "https://www.home-assistant.io/integrations/sabnzbd", "documentation": "https://www.home-assistant.io/integrations/sabnzbd",
"requirements": ["pysabnzbd==1.1.1"], "requirements": ["pysabnzbd==1.1.1"],
"dependencies": ["configurator"],
"after_dependencies": ["discovery"],
"codeowners": ["@shaiu"], "codeowners": ["@shaiu"],
"iot_class": "local_polling", "iot_class": "local_polling",
"config_flow": true, "config_flow": true,

View File

@ -14,8 +14,10 @@ from . import DOMAIN, SIGNAL_SABNZBD_UPDATED
from ...config_entries import ConfigEntry from ...config_entries import ConfigEntry
from ...const import DATA_GIGABYTES, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND from ...const import DATA_GIGABYTES, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND
from ...core import HomeAssistant from ...core import HomeAssistant
from ...helpers.device_registry import DeviceEntryType
from ...helpers.entity import DeviceInfo
from ...helpers.entity_platform import AddEntitiesCallback 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 @dataclass
@ -127,9 +129,16 @@ class SabnzbdSensor(SensorEntity):
self, sabnzbd_api_data, client_name, description: SabnzbdSensorEntityDescription self, sabnzbd_api_data, client_name, description: SabnzbdSensorEntityDescription
): ):
"""Initialize the sensor.""" """Initialize the sensor."""
unique_id = description.key
self._attr_unique_id = unique_id
self.entity_description = description self.entity_description = description
self._sabnzbd_api = sabnzbd_api_data self._sabnzbd_api = sabnzbd_api_data
self._attr_name = f"{client_name} {description.name}" 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): async def async_added_to_hass(self):
"""Call when entity about to be added to hass.""" """Call when entity about to be added to hass."""

View File

@ -30,7 +30,12 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.typing import ConfigType 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 ( from .const import (
CONF_ON_ACTION, CONF_ON_ACTION,
CONF_SESSION_ID, CONF_SESSION_ID,
@ -214,11 +219,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True 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( async def _async_create_bridge_with_updated_data(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: ConfigEntry
) -> SamsungTVBridge: ) -> SamsungTVBridge:
@ -279,7 +279,7 @@ async def _async_create_bridge_with_updated_data(
LOGGER.info("Updated model to %s for %s", model, host) LOGGER.info("Updated model to %s for %s", model, host)
updated_data[CONF_MODEL] = model 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( LOGGER.info(
"Detected model %s for %s. Some televisions from H and J series use " "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", "an encrypted protocol but you are using %s which may not be supported",

View File

@ -85,6 +85,11 @@ def mac_from_device_info(info: dict[str, Any]) -> str | None:
return 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( async def async_get_device_info(
hass: HomeAssistant, hass: HomeAssistant,
host: str, host: str,
@ -99,17 +104,19 @@ async def async_get_device_info(
port, port,
info, info,
) )
encrypted_bridge = SamsungTVEncryptedBridge( # Check the encrypted port if the model requires encryption
hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT if model_requires_encryption(info.get("device", {}).get("modelName")):
) encrypted_bridge = SamsungTVEncryptedBridge(
result = await encrypted_bridge.async_try_connect() hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT
if result != RESULT_CANNOT_CONNECT:
return (
result,
ENCRYPTED_WEBSOCKET_PORT,
METHOD_ENCRYPTED_WEBSOCKET,
info,
) )
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 return RESULT_SUCCESS, port, METHOD_WEBSOCKET, info
# Try legacy port # Try legacy port

View File

@ -6,7 +6,7 @@
"tensorflow==2.5.0", "tensorflow==2.5.0",
"tf-models-official==2.5.0", "tf-models-official==2.5.0",
"pycocotools==2.0.1", "pycocotools==2.0.1",
"numpy==1.21.4", "numpy==1.21.6",
"pillow==9.1.0" "pillow==9.1.0"
], ],
"codeowners": [], "codeowners": [],

View File

@ -2,7 +2,7 @@
"domain": "trend", "domain": "trend",
"name": "Trend", "name": "Trend",
"documentation": "https://www.home-assistant.io/integrations/trend", "documentation": "https://www.home-assistant.io/integrations/trend",
"requirements": ["numpy==1.21.4"], "requirements": ["numpy==1.21.6"],
"codeowners": [], "codeowners": [],
"quality_scale": "internal", "quality_scale": "internal",
"iot_class": "local_push" "iot_class": "local_push"

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 5 MINOR_VERSION: Final = 5
PATCH_VERSION: Final = "0" PATCH_VERSION: Final = "1"
__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

@ -711,6 +711,10 @@ class EntityRegistry:
if not valid_entity_id(entity["entity_id"]): if not valid_entity_id(entity["entity_id"]):
continue 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( entities[entity["entity_id"]] = RegistryEntry(
area_id=entity["area_id"], area_id=entity["area_id"],
capabilities=entity["capabilities"], capabilities=entity["capabilities"],

View File

@ -5,11 +5,13 @@ from collections.abc import Callable, Sequence
from typing import Any, TypedDict, cast from typing import Any, TypedDict, cast
import voluptuous as vol import voluptuous as vol
import yaml
from homeassistant.backports.enum import StrEnum from homeassistant.backports.enum import StrEnum
from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT
from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.core import split_entity_id, valid_entity_id
from homeassistant.util import decorator from homeassistant.util import decorator
from homeassistant.util.yaml.dumper import represent_odict
from . import config_validation as cv from . import config_validation as cv
@ -71,7 +73,11 @@ class Selector:
def serialize(self) -> Any: def serialize(self) -> Any:
"""Serialize Selector for voluptuous_serialize.""" """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( SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema(
@ -623,6 +629,13 @@ class NumberSelector(Selector):
"""Instantiate a selector.""" """Instantiate a selector."""
super().__init__(config) 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: def __call__(self, data: Any) -> float:
"""Validate the passed selection.""" """Validate the passed selection."""
value: float = vol.Coerce(float)(data) value: float = vol.Coerce(float)(data)
@ -881,3 +894,11 @@ class TimeSelector(Selector):
"""Validate the passed selection.""" """Validate the passed selection."""
cv.time(data) cv.time(data)
return cast(str, data) return cast(str, data)
yaml.SafeDumper.add_representer(
Selector,
lambda dumper, value: represent_odict(
dumper, "tag:yaml.org,2002:map", value.serialize()
),
)

View File

@ -110,7 +110,7 @@ aio_geojson_nsw_rfs_incidents==0.4
aio_georss_gdacs==0.7 aio_georss_gdacs==0.7
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.4.2 aioairzone==0.4.3
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
aioambient==2021.11.0 aioambient==2021.11.0
@ -1065,7 +1065,7 @@ netdisco==3.0.0
netmap==0.7.0.2 netmap==0.7.0.2
# homeassistant.components.nam # homeassistant.components.nam
nettigo-air-monitor==1.2.2 nettigo-air-monitor==1.2.3
# homeassistant.components.neurio_energy # homeassistant.components.neurio_energy
neurio==0.3.1 neurio==0.3.1
@ -1111,7 +1111,7 @@ numato-gpio==0.10.0
# homeassistant.components.opencv # homeassistant.components.opencv
# homeassistant.components.tensorflow # homeassistant.components.tensorflow
# homeassistant.components.trend # homeassistant.components.trend
numpy==1.21.4 numpy==1.21.6
# homeassistant.components.oasa_telematics # homeassistant.components.oasa_telematics
oasatelematics==0.3 oasatelematics==0.3
@ -1399,7 +1399,7 @@ pycfdns==1.2.2
pychannels==1.0.0 pychannels==1.0.0
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==12.0.0 pychromecast==12.1.1
# homeassistant.components.pocketcasts # homeassistant.components.pocketcasts
pycketcasts==1.0.0 pycketcasts==1.0.0

View File

@ -94,7 +94,7 @@ aio_geojson_nsw_rfs_incidents==0.4
aio_georss_gdacs==0.7 aio_georss_gdacs==0.7
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.4.2 aioairzone==0.4.3
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
aioambient==2021.11.0 aioambient==2021.11.0
@ -727,7 +727,7 @@ netdisco==3.0.0
netmap==0.7.0.2 netmap==0.7.0.2
# homeassistant.components.nam # homeassistant.components.nam
nettigo-air-monitor==1.2.2 nettigo-air-monitor==1.2.3
# homeassistant.components.nexia # homeassistant.components.nexia
nexia==0.9.13 nexia==0.9.13
@ -755,7 +755,7 @@ numato-gpio==0.10.0
# homeassistant.components.opencv # homeassistant.components.opencv
# homeassistant.components.tensorflow # homeassistant.components.tensorflow
# homeassistant.components.trend # homeassistant.components.trend
numpy==1.21.4 numpy==1.21.6
# homeassistant.components.google # homeassistant.components.google
oauth2client==4.1.3 oauth2client==4.1.3
@ -938,7 +938,7 @@ pybotvac==0.0.23
pycfdns==1.2.2 pycfdns==1.2.2
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==12.0.0 pychromecast==12.1.1
# homeassistant.components.climacell # homeassistant.components.climacell
pyclimacell==0.18.2 pyclimacell==0.18.2

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = homeassistant name = homeassistant
version = 2022.5.0 version = 2022.5.1
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

@ -147,7 +147,7 @@ async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None:
] ]
} }
with patch( with patch(
"homeassistant.components.airzone.AirzoneLocalApi.http_request", "homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
return_value=HVAC_MOCK, return_value=HVAC_MOCK,
): ):
await hass.services.async_call( await hass.services.async_call(
@ -172,7 +172,7 @@ async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None:
] ]
} }
with patch( with patch(
"homeassistant.components.airzone.AirzoneLocalApi.http_request", "homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
return_value=HVAC_MOCK, return_value=HVAC_MOCK,
): ):
await hass.services.async_call( await hass.services.async_call(
@ -204,7 +204,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None:
] ]
} }
with patch( with patch(
"homeassistant.components.airzone.AirzoneLocalApi.http_request", "homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
return_value=HVAC_MOCK, return_value=HVAC_MOCK,
): ):
await hass.services.async_call( await hass.services.async_call(
@ -230,7 +230,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None:
] ]
} }
with patch( with patch(
"homeassistant.components.airzone.AirzoneLocalApi.http_request", "homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
return_value=HVAC_MOCK_2, return_value=HVAC_MOCK_2,
): ):
await hass.services.async_call( 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) await async_init_integration(hass)
with patch( with patch(
"homeassistant.components.airzone.AirzoneLocalApi.http_request", "homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
return_value=HVAC_MOCK, return_value=HVAC_MOCK,
), pytest.raises(HomeAssistantError): ), pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
@ -296,7 +296,7 @@ async def test_airzone_climate_set_temp(hass: HomeAssistant) -> None:
await async_init_integration(hass) await async_init_integration(hass)
with patch( with patch(
"homeassistant.components.airzone.AirzoneLocalApi.http_request", "homeassistant.components.airzone.AirzoneLocalApi.put_hvac",
return_value=HVAC_MOCK, return_value=HVAC_MOCK,
): ):
await hass.services.async_call( await hass.services.async_call(

View File

@ -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.domain == "automation"
assert imported_blueprint.blueprint.inputs == { assert imported_blueprint.blueprint.inputs == {
"service_to_call": None, "service_to_call": None,
"trigger_event": None, "trigger_event": {"selector": {"text": {}}},
} }
assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.suggested_filename == "balloob/motion_light"
assert imported_blueprint.blueprint.metadata["source_url"] == url assert imported_blueprint.blueprint.metadata["source_url"] == url

View File

@ -30,7 +30,10 @@ async def test_list_blueprints(hass, hass_ws_client):
"test_event_service.yaml": { "test_event_service.yaml": {
"metadata": { "metadata": {
"domain": "automation", "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", "name": "Call service based on event",
}, },
}, },
@ -89,7 +92,10 @@ async def test_import_blueprint(hass, aioclient_mock, hass_ws_client):
"blueprint": { "blueprint": {
"metadata": { "metadata": {
"domain": "automation", "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", "name": "Call service based on event",
"source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", "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 msg["success"]
assert write_mock.mock_calls assert write_mock.mock_calls
assert write_mock.call_args[0] == ( 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",
) )

View File

@ -14,6 +14,13 @@ def get_multizone_status_mock():
return 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() @pytest.fixture()
def castbrowser_mock(): def castbrowser_mock():
"""Mock pychromecast CastBrowser.""" """Mock pychromecast CastBrowser."""
@ -43,6 +50,7 @@ def cast_mock(
mz_mock, mz_mock,
quick_play_mock, quick_play_mock,
castbrowser_mock, castbrowser_mock,
get_cast_type_mock,
get_chromecast_mock, get_chromecast_mock,
get_multizone_status_mock, get_multizone_status_mock,
): ):
@ -52,6 +60,9 @@ def cast_mock(
with patch( with patch(
"homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser", "homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser",
castbrowser_mock, castbrowser_mock,
), patch(
"homeassistant.components.cast.helpers.dial.get_cast_type",
get_cast_type_mock,
), patch( ), patch(
"homeassistant.components.cast.helpers.dial.get_multizone_status", "homeassistant.components.cast.helpers.dial.get_multizone_status",
get_multizone_status_mock, get_multizone_status_mock,

View File

@ -64,6 +64,8 @@ FAKE_MDNS_SERVICE = pychromecast.discovery.ServiceInfo(
pychromecast.const.SERVICE_TYPE_MDNS, "the-service" pychromecast.const.SERVICE_TYPE_MDNS, "the-service"
) )
UNDEFINED = object()
def get_fake_chromecast(info: ChromecastInfo): def get_fake_chromecast(info: ChromecastInfo):
"""Generate a Fake Chromecast object with the specified arguments.""" """Generate a Fake Chromecast object with the specified arguments."""
@ -74,7 +76,14 @@ def get_fake_chromecast(info: ChromecastInfo):
def get_fake_chromecast_info( 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.""" """Generate a Fake ChromecastInfo with the specified arguments."""
@ -82,16 +91,22 @@ def get_fake_chromecast_info(
service = pychromecast.discovery.ServiceInfo( service = pychromecast.discovery.ServiceInfo(
pychromecast.const.SERVICE_TYPE_HOST, (host, port) 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( return ChromecastInfo(
cast_info=pychromecast.models.CastInfo( cast_info=pychromecast.models.CastInfo(
services={service}, services={service},
uuid=uuid, uuid=uuid,
model_name="Chromecast", model_name=model_name,
friendly_name="Speaker", friendly_name="Speaker",
host=host, host=host,
port=port, port=port,
cast_type=CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST, cast_type=cast_type,
manufacturer="Nabu Casa", manufacturer=manufacturer,
) )
) )
@ -342,6 +357,92 @@ async def test_internal_discovery_callback_fill_out_group(
get_multizone_status_mock.assert_called_once() 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): async def test_stop_discovery_called_on_stop(hass, castbrowser_mock):
"""Test pychromecast.stop_discovery called on shutdown.""" """Test pychromecast.stop_discovery called on shutdown."""
# start_discovery should be called with empty config # start_discovery should be called with empty config

View File

@ -22,7 +22,7 @@ from samsungtvws.remote import ChannelEmitCommand
from homeassistant.components.samsungtv.const import WEBSOCKET_SSL_PORT from homeassistant.components.samsungtv.const import WEBSOCKET_SSL_PORT
import homeassistant.util.dt as dt_util 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) @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.""" """Mock rest_device_info to fail for ssl and work for non-ssl."""
if self.port == WEBSOCKET_SSL_PORT: if self.port == WEBSOCKET_SSL_PORT:
raise ResponseError raise ResponseError
return SAMPLE_DEVICE_INFO_WIFI return SAMPLE_DEVICE_INFO_UE48JU6400
with patch( with patch(
"homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest",

View File

@ -340,16 +340,16 @@ async def test_user_encrypted_websocket(
) )
assert result4["type"] == "create_entry" 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_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_MAC] == "aa:bb:ww:ii:ff:ii"
assert result4["data"][CONF_MANUFACTURER] == "Samsung" 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_SSDP_RENDERING_CONTROL_LOCATION] is None
assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72"
assert result4["data"][CONF_SESSION_ID] == "1" 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") @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["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_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_MAC] == "aa:bb:ww:ii:ff:ii"
assert result4["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result4["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer"
assert result4["data"][CONF_MODEL] == "82GXARRS" assert result4["data"][CONF_MODEL] == "UE48JU6400"
assert ( assert (
result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION]
== "https://fake_host:12345/test" == "https://fake_host:12345/test"
) )
assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72"
assert result4["data"][CONF_SESSION_ID] == "1" 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") @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" result["flow_id"], user_input="whatever"
) )
assert result["type"] == "create_entry" 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_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_MAC] == "aa:bb:ww:ii:ff:ii"
assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MANUFACTURER] == "Samsung"
assert result["data"][CONF_MODEL] == "82GXARRS" assert result["data"][CONF_MODEL] == "UE48JU6400"
assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" assert result["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3"
@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing")

View File

@ -11,7 +11,7 @@ import pytest
import voluptuous as vol import voluptuous as vol
import homeassistant import homeassistant
from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers import config_validation as cv, selector, template
def test_boolean(): 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(): def test_positive_time_period_dict_in_serializer():
"""Test positive_time_period_dict with custom_serializer.""" """Test positive_time_period_dict with custom_serializer."""
assert cv.custom_serializer(cv.positive_time_period_dict) == { assert cv.custom_serializer(cv.positive_time_period_dict) == {

View File

@ -297,6 +297,12 @@ async def test_loading_extra_values(hass, hass_storage):
"unique_id": "invalid-hass", "unique_id": "invalid-hass",
"disabled_by": er.RegistryEntryDisabler.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) await er.async_load(hass)
registry = er.async_get(hass) registry = er.async_get(hass)
assert len(registry.entities) == 4 assert len(registry.entities) == 5
entry_with_name = registry.async_get_or_create( entry_with_name = registry.async_get_or_create(
"test", "super_platform", "with-name" "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
assert entry_disabled_user.disabled_by is er.RegistryEntryDisabler.USER 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): def test_async_get_entity_id(registry):
"""Test that entity_id is returned.""" """Test that entity_id is returned."""

View File

@ -2,7 +2,8 @@
import pytest import pytest
import voluptuous as vol 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" FAKE_UUID = "a266a680b608c32770e6c45bfe6b8411"
@ -48,10 +49,12 @@ def _test_selector(
converter = default_converter converter = default_converter
# Validate selector configuration # 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 # 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: for selection in valid_selections:
assert vol_schema({"selection": selection}) == { assert vol_schema({"selection": selection}) == {
"selection": converter(selection) "selection": converter(selection)
@ -62,9 +65,12 @@ def _test_selector(
# Serialize selector # Serialize selector
selector_instance = selector.selector({selector_type: schema}) selector_instance = selector.selector({selector_type: schema})
assert cv.custom_serializer(selector_instance) == { assert (
"selector": {selector_type: selector_instance.config} 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( @pytest.mark.parametrize(

View File

@ -3,6 +3,8 @@ blueprint:
domain: automation domain: automation
input: input:
trigger_event: trigger_event:
selector:
text:
service_to_call: service_to_call:
trigger: trigger:
platform: event platform: event