This commit is contained in:
Paulus Schoutsen
2023-07-06 14:24:50 -04:00
committed by GitHub
25 changed files with 263 additions and 73 deletions

View File

@@ -34,6 +34,7 @@ class FlickPricingSensor(SensorEntity):
_attr_attribution = "Data provided by Flick Electric"
_attr_native_unit_of_measurement = f"{CURRENCY_CENT}/{UnitOfEnergy.KILO_WATT_HOUR}"
_attr_has_entity_name = True
_attr_translation_key = "power_price"
_attributes: dict[str, Any] = {}

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230705.0"]
"requirements": ["home-assistant-frontend==20230705.1"]
}

View File

@@ -128,7 +128,7 @@ class MatterLight(MatterEntity, LightEntity):
renormalize(
brightness,
(0, 255),
(level_control.minLevel, level_control.maxLevel),
(level_control.minLevel or 1, level_control.maxLevel or 254),
)
)
@@ -220,7 +220,7 @@ class MatterLight(MatterEntity, LightEntity):
return round(
renormalize(
level_control.currentLevel,
(level_control.minLevel or 0, level_control.maxLevel or 254),
(level_control.minLevel or 1, level_control.maxLevel or 254),
(0, 255),
)
)

View File

@@ -72,6 +72,7 @@ class NukiDeviceEntity(NukiEntity[_NukiDeviceT], LockEntity):
_attr_has_entity_name = True
_attr_supported_features = LockEntityFeature.OPEN
_attr_translation_key = "nuki_lock"
_attr_name = None
@property
def unique_id(self) -> str | None:

View File

@@ -19,5 +19,11 @@
"invalid_auth": "Bad authentication",
"unknown": "Unknown error"
}
},
"issues": {
"deprecated_yaml": {
"title": "The QNAP YAML configuration is being removed",
"description": "Configuring QNAP using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the QNAP YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
}
}
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/rainbird",
"iot_class": "local_polling",
"loggers": ["pyrainbird"],
"requirements": ["pyrainbird==2.0.0"]
"requirements": ["pyrainbird==2.1.0"]
}

View File

@@ -608,7 +608,7 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity):
variables=service.data, context=service.context, wait=True
)
if service.return_response:
return response
return response or {}
return None
async def async_added_to_hass(self) -> None:

View File

@@ -27,8 +27,10 @@ from homeassistant.util.dt import utcnow
from .const import DEFAULT_NAME, DOMAIN, PLAYER_EVENT
STATE_MAPPING = {
PlayerState.IDLE: MediaPlayerState.IDLE,
PlayerState.STOPPED: MediaPlayerState.IDLE,
PlayerState.PLAYING: MediaPlayerState.PLAYING,
PlayerState.BUFFER_READY: MediaPlayerState.PLAYING,
PlayerState.BUFFERING: MediaPlayerState.PLAYING,
PlayerState.PAUSED: MediaPlayerState.PAUSED,
}

View File

@@ -44,7 +44,7 @@
},
"abort": {
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"no_devices_found": "No supported SwitchBot devices found in range; If the device is in range, ensure the scanner has active scanning enabled, as SwitchBot devices cannot be discovered with passive scans. Active scans can be disabled once the device is configured. If you need clarification on whether the device is in-range, download the diagnostics for the integration that provides your Bluetooth adapter or proxy and check if the MAC address of the SwitchBot device is present.",
"unknown": "[%key:common::config_flow::error::unknown%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"switchbot_unsupported_type": "Unsupported Switchbot Type."

View File

@@ -511,6 +511,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
"rqbj": (
TuyaSensorEntityDescription(
key=DPCode.GAS_SENSOR_VALUE,
name=None,
icon="mdi:gas-cylinder",
state_class=SensorStateClass.MEASUREMENT,
),
@@ -633,6 +634,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
"ylcg": (
TuyaSensorEntityDescription(
key=DPCode.PRESSURE_VALUE,
name=None,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
),

View File

@@ -292,6 +292,8 @@ async def async_setup_entry(
class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity):
"""Representation of a generic Xiaomi device."""
_attr_name = None
def __init__(self, device, entry, unique_id, coordinator):
"""Initialize the generic Xiaomi device."""
super().__init__(device, entry, unique_id, coordinator)

View File

@@ -118,6 +118,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity):
_attr_device_class = HumidifierDeviceClass.HUMIDIFIER
_attr_supported_features = HumidifierEntityFeature.MODES
_attr_name = None
def __init__(self, device, entry, unique_id, coordinator):
"""Initialize the generic Xiaomi device."""

View File

@@ -130,14 +130,11 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity):
@property
def _is_netusb(self):
return (
self.coordinator.data.netusb_input
== self.coordinator.data.zones[self._zone_id].input
)
return self.coordinator.data.netusb_input == self.source_id
@property
def _is_tuner(self):
return self.coordinator.data.zones[self._zone_id].input == "tuner"
return self.source_id == "tuner"
@property
def media_content_id(self):
@@ -516,10 +513,15 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity):
self._zone_id, self.reverse_source_mapping.get(source, source)
)
@property
def source_id(self):
"""ID of the current input source."""
return self.coordinator.data.zones[self._zone_id].input
@property
def source(self):
"""Name of the current input source."""
return self.source_mapping.get(self.coordinator.data.zones[self._zone_id].input)
return self.source_mapping.get(self.source_id)
@property
def source_list(self):
@@ -597,7 +599,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity):
return (
self.coordinator.data.group_role == "client"
and self.coordinator.data.group_id != NULL_GROUP
and self.source == ATTR_MC_LINK
and self.source_id == ATTR_MC_LINK
)
@property
@@ -606,7 +608,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity):
If the media player is not part of a group, False is returned.
"""
return self.is_network_client or self.source == ATTR_MAIN_SYNC
return self.is_network_client or self.source_id == ATTR_MAIN_SYNC
def get_all_mc_entities(self) -> list[MusicCastMediaPlayer]:
"""Return all media player entities of the musiccast system."""
@@ -639,11 +641,11 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity):
and self.coordinator.data.group_id
== group_server.coordinator.data.group_id
and self.ip_address != group_server.ip_address
and self.source == ATTR_MC_LINK
and self.source_id == ATTR_MC_LINK
)
or (
self.ip_address == group_server.ip_address
and self.source == ATTR_MAIN_SYNC
and self.source_id == ATTR_MAIN_SYNC
)
)
@@ -859,8 +861,12 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity):
"""
_LOGGER.debug("%s client leave called", self.entity_id)
if not force and (
self.source == ATTR_MAIN_SYNC
or [entity for entity in self.other_zones if entity.source == ATTR_MC_LINK]
self.source_id == ATTR_MAIN_SYNC
or [
entity
for entity in self.other_zones
if entity.source_id == ATTR_MC_LINK
]
):
await self.coordinator.musiccast.zone_unjoin(self._zone_id)
else:

View File

@@ -3,6 +3,7 @@ import asyncio
import copy
import logging
import os
import re
import voluptuous as vol
from zhaquirks import setup as setup_quirks
@@ -85,19 +86,34 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
def _clean_serial_port_path(path: str) -> str:
"""Clean the serial port path, applying corrections where necessary."""
if path.startswith("socket://"):
path = path.strip()
# Removes extraneous brackets from IP addresses (they don't parse in CPython 3.11.4)
if re.match(r"^socket://\[\d+\.\d+\.\d+\.\d+\]:\d+$", path):
path = path.replace("[", "").replace("]", "")
return path
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up ZHA.
Will automatically load components to support devices found on the network.
"""
# Strip whitespace around `socket://` URIs, this is no longer accepted by zigpy
# This will be removed in 2023.7.0
# Remove brackets around IP addresses, this no longer works in CPython 3.11.4
# This will be removed in 2023.11.0
path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
cleaned_path = _clean_serial_port_path(path)
data = copy.deepcopy(dict(config_entry.data))
if path.startswith("socket://") and path != path.strip():
data[CONF_DEVICE][CONF_DEVICE_PATH] = path.strip()
if path != cleaned_path:
_LOGGER.debug("Cleaned serial port path %r -> %r", path, cleaned_path)
data[CONF_DEVICE][CONF_DEVICE_PATH] = cleaned_path
hass.config_entries.async_update_entry(config_entry, data=data)
zha_data = hass.data.setdefault(DATA_ZHA, {})

View File

@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 7
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, 10, 0)

View File

@@ -772,9 +772,10 @@ class Entity(ABC):
):
return name
device_name = device_entry.name_by_user or device_entry.name
if self.use_device_name:
return device_entry.name_by_user or device_entry.name
return f"{device_entry.name_by_user or device_entry.name} {name}"
return device_name
return f"{device_name} {name}" if device_name else name
@callback
def _async_write_ha_state(self) -> None:

View File

@@ -566,7 +566,9 @@ async def async_get_all_descriptions(
hass: HomeAssistant,
) -> dict[str, dict[str, Any]]:
"""Return descriptions (i.e. user documentation) for all service calls."""
descriptions_cache = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {})
descriptions_cache: dict[
tuple[str, str], dict[str, Any] | None
] = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {})
services = hass.services.async_services()
# See if there are new services not seen before.
@@ -574,59 +576,60 @@ async def async_get_all_descriptions(
missing = set()
all_services = []
for domain in services:
for service in services[domain]:
cache_key = (domain, service)
for service_name in services[domain]:
cache_key = (domain, service_name)
all_services.append(cache_key)
if cache_key not in descriptions_cache:
missing.add(domain)
# If we have a complete cache, check if it is still valid
if ALL_SERVICE_DESCRIPTIONS_CACHE in hass.data:
previous_all_services, previous_descriptions_cache = hass.data[
ALL_SERVICE_DESCRIPTIONS_CACHE
]
if all_cache := hass.data.get(ALL_SERVICE_DESCRIPTIONS_CACHE):
previous_all_services, previous_descriptions_cache = all_cache
# If the services are the same, we can return the cache
if previous_all_services == all_services:
return cast(dict[str, dict[str, Any]], previous_descriptions_cache)
# Files we loaded for missing descriptions
loaded = {}
loaded: dict[str, JSON_TYPE] = {}
if missing:
ints_or_excs = await async_get_integrations(hass, missing)
integrations = [
int_or_exc
for int_or_exc in ints_or_excs.values()
if isinstance(int_or_exc, Integration)
]
integrations: list[Integration] = []
for domain, int_or_exc in ints_or_excs.items():
if type(int_or_exc) is Integration: # pylint: disable=unidiomatic-typecheck
integrations.append(int_or_exc)
continue
if TYPE_CHECKING:
assert isinstance(int_or_exc, Exception)
_LOGGER.error("Failed to load integration: %s", domain, exc_info=int_or_exc)
contents = await hass.async_add_executor_job(
_load_services_files, hass, integrations
)
for domain, content in zip(missing, contents):
loaded[domain] = content
loaded = dict(zip(missing, contents))
# Build response
descriptions: dict[str, dict[str, Any]] = {}
for domain in services:
for domain, services_map in services.items():
descriptions[domain] = {}
domain_descriptions = descriptions[domain]
for service in services[domain]:
cache_key = (domain, service)
for service_name in services_map:
cache_key = (domain, service_name)
description = descriptions_cache.get(cache_key)
# Cache missing descriptions
if description is None:
domain_yaml = loaded[domain]
domain_yaml = loaded.get(domain) or {}
# The YAML may be empty for dynamically defined
# services (ie shell_command) that never call
# service.async_set_service_schema for the dynamic
# service
yaml_description = domain_yaml.get( # type: ignore[union-attr]
service, {}
service_name, {}
)
# Don't warn for missing services, because it triggers false
# positives for things like scripts, that register as a service
description = {
"name": yaml_description.get("name", ""),
"description": yaml_description.get("description", ""),
@@ -637,7 +640,7 @@ async def async_get_all_descriptions(
description["target"] = yaml_description["target"]
if (
response := hass.services.supports_response(domain, service)
response := hass.services.supports_response(domain, service_name)
) != SupportsResponse.NONE:
description["response"] = {
"optional": response == SupportsResponse.OPTIONAL,
@@ -645,7 +648,7 @@ async def async_get_all_descriptions(
descriptions_cache[cache_key] = description
descriptions[domain][service] = description
domain_descriptions[service_name] = description
hass.data[ALL_SERVICE_DESCRIPTIONS_CACHE] = (all_services, descriptions)
return descriptions
@@ -667,7 +670,9 @@ def async_set_service_schema(
hass: HomeAssistant, domain: str, service: str, schema: dict[str, Any]
) -> None:
"""Register a description for a service."""
hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {})
descriptions_cache: dict[
tuple[str, str], dict[str, Any] | None
] = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {})
description = {
"name": schema.get("name", ""),
@@ -678,8 +683,15 @@ def async_set_service_schema(
if "target" in schema:
description["target"] = schema["target"]
if (
response := hass.services.supports_response(domain, service)
) != SupportsResponse.NONE:
description["response"] = {
"optional": response == SupportsResponse.OPTIONAL,
}
hass.data.pop(ALL_SERVICE_DESCRIPTIONS_CACHE, None)
hass.data[SERVICE_DESCRIPTION_CACHE][(domain, service)] = description
descriptions_cache[(domain, service)] = description
@bind_hass

View File

@@ -22,7 +22,7 @@ ha-av==10.1.0
hass-nabucasa==0.69.0
hassil==1.0.6
home-assistant-bluetooth==1.10.0
home-assistant-frontend==20230705.0
home-assistant-frontend==20230705.1
home-assistant-intents==2023.6.28
httpx==0.24.1
ifaddr==0.2.0

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.7.0"
version = "2023.7.1"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

@@ -980,7 +980,7 @@ hole==0.8.0
holidays==0.21.13
# homeassistant.components.frontend
home-assistant-frontend==20230705.0
home-assistant-frontend==20230705.1
# homeassistant.components.conversation
home-assistant-intents==2023.6.28
@@ -1938,7 +1938,7 @@ pyqwikswitch==0.93
pyrail==0.0.3
# homeassistant.components.rainbird
pyrainbird==2.0.0
pyrainbird==2.1.0
# homeassistant.components.recswitch
pyrecswitch==1.0.2

View File

@@ -763,7 +763,7 @@ hole==0.8.0
holidays==0.21.13
# homeassistant.components.frontend
home-assistant-frontend==20230705.0
home-assistant-frontend==20230705.1
# homeassistant.components.conversation
home-assistant-intents==2023.6.28
@@ -1439,7 +1439,7 @@ pyps4-2ndscreen==1.3.1
pyqwikswitch==0.93
# homeassistant.components.rainbird
pyrainbird==2.0.0
pyrainbird==2.1.0
# homeassistant.components.risco
pyrisco==0.5.7

View File

@@ -26,7 +26,7 @@ from homeassistant.core import (
callback,
split_entity_id,
)
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound
from homeassistant.exceptions import ServiceNotFound
from homeassistant.helpers import entity_registry as er, template
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.script import (
@@ -1625,7 +1625,7 @@ async def test_responses(hass: HomeAssistant, response: Any) -> None:
)
async def test_responses_error(hass: HomeAssistant) -> None:
async def test_responses_no_response(hass: HomeAssistant) -> None:
"""Test response variable not set."""
mock_restore_cache(hass, ())
assert await async_setup_component(
@@ -1645,10 +1645,13 @@ async def test_responses_error(hass: HomeAssistant) -> None:
},
)
with pytest.raises(HomeAssistantError):
assert await hass.services.async_call(
# Validate we can call it with return_response
assert (
await hass.services.async_call(
DOMAIN, "test", {"greeting": "world"}, blocking=True, return_response=True
)
== {}
)
# Validate we can also call it without return_response
assert (
await hass.services.async_call(

View File

@@ -114,19 +114,27 @@ async def test_config_depreciation(hass: HomeAssistant, zha_config) -> None:
@pytest.mark.parametrize(
("path", "cleaned_path"),
[
# No corrections
("/dev/path1", "/dev/path1"),
("/dev/path1[asd]", "/dev/path1[asd]"),
("/dev/path1 ", "/dev/path1 "),
("socket://1.2.3.4:5678", "socket://1.2.3.4:5678"),
# Brackets around URI
("socket://[1.2.3.4]:5678", "socket://1.2.3.4:5678"),
# Spaces
("socket://dev/path1 ", "socket://dev/path1"),
# Both
("socket://[1.2.3.4]:5678 ", "socket://1.2.3.4:5678"),
],
)
@patch("homeassistant.components.zha.setup_quirks", Mock(return_value=True))
@patch(
"homeassistant.components.zha.websocket_api.async_load_api", Mock(return_value=True)
)
async def test_setup_with_v3_spaces_in_uri(
async def test_setup_with_v3_cleaning_uri(
hass: HomeAssistant, path: str, cleaned_path: str
) -> None:
"""Test migration of config entry from v3 with spaces after `socket://` URI."""
"""Test migration of config entry from v3, applying corrections to the port path."""
config_entry_v3 = MockConfigEntry(
domain=DOMAIN,
data={

View File

@@ -20,7 +20,7 @@ from homeassistant.const import (
from homeassistant.core import Context, HomeAssistant, HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity, entity_registry as er
from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.helpers.typing import UNDEFINED
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from tests.common import (
MockConfigEntry,
@@ -989,12 +989,21 @@ async def _test_friendly_name(
@pytest.mark.parametrize(
("has_entity_name", "entity_name", "expected_friendly_name", "warn_implicit_name"),
(
(False, "Entity Blu", "Entity Blu", False),
(False, None, None, False),
(True, "Entity Blu", "Device Bla Entity Blu", False),
(True, None, "Device Bla", False),
"has_entity_name",
"entity_name",
"device_name",
"expected_friendly_name",
"warn_implicit_name",
),
(
(False, "Entity Blu", "Device Bla", "Entity Blu", False),
(False, None, "Device Bla", None, False),
(True, "Entity Blu", "Device Bla", "Device Bla Entity Blu", False),
(True, None, "Device Bla", "Device Bla", False),
(True, "Entity Blu", UNDEFINED, "Entity Blu", False),
# Not valid on RC
# (True, "Entity Blu", None, "Mock Title Entity Blu", False),
),
)
async def test_friendly_name_attr(
@@ -1002,6 +1011,7 @@ async def test_friendly_name_attr(
caplog: pytest.LogCaptureFixture,
has_entity_name: bool,
entity_name: str | None,
device_name: str | None | UndefinedType,
expected_friendly_name: str | None,
warn_implicit_name: bool,
) -> None:
@@ -1012,7 +1022,7 @@ async def test_friendly_name_attr(
device_info={
"identifiers": {("hue", "1234")},
"connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")},
"name": "Device Bla",
"name": device_name,
},
)
ent._attr_has_entity_name = has_entity_name

View File

@@ -572,6 +572,98 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None:
assert "description" in descriptions[logger.DOMAIN]["new_service"]
assert descriptions[logger.DOMAIN]["new_service"]["description"] == "new service"
hass.services.async_register(
logger.DOMAIN, "another_new_service", lambda x: None, None
)
hass.services.async_register(
logger.DOMAIN,
"service_with_optional_response",
lambda x: None,
None,
SupportsResponse.OPTIONAL,
)
hass.services.async_register(
logger.DOMAIN,
"service_with_only_response",
lambda x: None,
None,
SupportsResponse.ONLY,
)
hass.services.async_register(
logger.DOMAIN,
"another_service_with_response",
lambda x: None,
None,
SupportsResponse.OPTIONAL,
)
service.async_set_service_schema(
hass,
logger.DOMAIN,
"another_service_with_response",
{"description": "response service"},
)
descriptions = await service.async_get_all_descriptions(hass)
assert "another_new_service" in descriptions[logger.DOMAIN]
assert "service_with_optional_response" in descriptions[logger.DOMAIN]
assert descriptions[logger.DOMAIN]["service_with_optional_response"][
"response"
] == {"optional": True}
assert "service_with_only_response" in descriptions[logger.DOMAIN]
assert descriptions[logger.DOMAIN]["service_with_only_response"]["response"] == {
"optional": False
}
assert "another_service_with_response" in descriptions[logger.DOMAIN]
assert descriptions[logger.DOMAIN]["another_service_with_response"]["response"] == {
"optional": True
}
# Verify the cache returns the same object
assert await service.async_get_all_descriptions(hass) is descriptions
async def test_async_get_all_descriptions_failing_integration(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test async_get_all_descriptions when async_get_integrations returns an exception."""
group = hass.components.group
group_config = {group.DOMAIN: {}}
await async_setup_component(hass, group.DOMAIN, group_config)
descriptions = await service.async_get_all_descriptions(hass)
assert len(descriptions) == 1
assert "description" in descriptions["group"]["reload"]
assert "fields" in descriptions["group"]["reload"]
logger = hass.components.logger
logger_config = {logger.DOMAIN: {}}
await async_setup_component(hass, logger.DOMAIN, logger_config)
with patch(
"homeassistant.helpers.service.async_get_integrations",
return_value={"logger": ImportError},
):
descriptions = await service.async_get_all_descriptions(hass)
assert len(descriptions) == 2
assert "Failed to load integration: logger" in caplog.text
# Services are empty defaults if the load fails but should
# not raise
assert descriptions[logger.DOMAIN]["set_level"] == {
"description": "",
"fields": {},
"name": "",
}
hass.services.async_register(logger.DOMAIN, "new_service", lambda x: None, None)
service.async_set_service_schema(
hass, logger.DOMAIN, "new_service", {"description": "new service"}
)
descriptions = await service.async_get_all_descriptions(hass)
assert "description" in descriptions[logger.DOMAIN]["new_service"]
assert descriptions[logger.DOMAIN]["new_service"]["description"] == "new service"
hass.services.async_register(
logger.DOMAIN, "another_new_service", lambda x: None, None
)
@@ -605,6 +697,33 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None:
assert await service.async_get_all_descriptions(hass) is descriptions
async def test_async_get_all_descriptions_dynamically_created_services(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test async_get_all_descriptions when async_get_integrations when services are dynamic."""
group = hass.components.group
group_config = {group.DOMAIN: {}}
await async_setup_component(hass, group.DOMAIN, group_config)
descriptions = await service.async_get_all_descriptions(hass)
assert len(descriptions) == 1
assert "description" in descriptions["group"]["reload"]
assert "fields" in descriptions["group"]["reload"]
shell_command = hass.components.shell_command
shell_command_config = {shell_command.DOMAIN: {"test_service": "ls /bin"}}
await async_setup_component(hass, shell_command.DOMAIN, shell_command_config)
descriptions = await service.async_get_all_descriptions(hass)
assert len(descriptions) == 2
assert descriptions[shell_command.DOMAIN]["test_service"] == {
"description": "",
"fields": {},
"name": "",
}
async def test_call_with_required_features(hass: HomeAssistant, mock_entities) -> None:
"""Test service calls invoked only if entity has required features."""
test_service_mock = AsyncMock(return_value=None)