Merge branch 'dev' into single_history_query

This commit is contained in:
J. Nick Koston
2023-04-11 08:54:25 -10:00
committed by GitHub
49 changed files with 637 additions and 280 deletions

View File

@@ -32,6 +32,7 @@ from .const import DEFAULT_EXPOSED_ATTRIBUTES, DEFAULT_EXPOSED_DOMAINS, DOMAIN
_LOGGER = logging.getLogger(__name__)
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
_ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"]
REGEX_TYPE = type(re.compile(""))
@@ -450,8 +451,10 @@ class DefaultAgent(AbstractConversationAgent):
@core.callback
def _async_handle_entity_registry_changed(self, event: core.Event) -> None:
"""Clear names list cache when an entity changes aliases."""
if event.data["action"] == "update" and "aliases" not in event.data["changes"]:
"""Clear names list cache when an entity registry entry has changed."""
if event.data["action"] == "update" and not any(
field in event.data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS
):
return
self._slot_lists = None

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/doods",
"iot_class": "local_polling",
"loggers": ["pydoods"],
"requirements": ["pydoods==1.0.2", "pillow==9.4.0"]
"requirements": ["pydoods==1.0.2", "pillow==9.5.0"]
}

View File

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

View File

@@ -6,5 +6,5 @@
"dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/generic",
"iot_class": "local_push",
"requirements": ["ha-av==10.0.0", "pillow==9.4.0"]
"requirements": ["ha-av==10.0.0", "pillow==9.5.0"]
}

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/image_upload",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["pillow==9.4.0"]
"requirements": ["pillow==9.5.0"]
}

View File

@@ -24,7 +24,7 @@ from homeassistant.const import (
SERVICE_RELOAD,
)
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryError, TemplateError, Unauthorized
from homeassistant.exceptions import TemplateError, Unauthorized
from homeassistant.helpers import config_validation as cv, event, template
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -45,7 +45,7 @@ from .client import ( # noqa: F401
publish,
subscribe,
)
from .config_integration import CONFIG_SCHEMA_ENTRY, PLATFORM_CONFIG_SCHEMA_BASE
from .config_integration import PLATFORM_CONFIG_SCHEMA_BASE
from .const import ( # noqa: F401
ATTR_PAYLOAD,
ATTR_QOS,
@@ -68,7 +68,9 @@ from .const import ( # noqa: F401
CONF_WS_HEADERS,
CONF_WS_PATH,
DATA_MQTT,
DEFAULT_DISCOVERY,
DEFAULT_ENCODING,
DEFAULT_PREFIX,
DEFAULT_QOS,
DEFAULT_RETAIN,
DOMAIN,
@@ -178,7 +180,9 @@ async def _async_setup_discovery(
This method is a coroutine.
"""
await discovery.async_start(hass, conf[CONF_DISCOVERY_PREFIX], config_entry)
await discovery.async_start(
hass, conf.get(CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX), config_entry
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -198,15 +202,8 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Load a config entry."""
# validate entry config
try:
conf = CONFIG_SCHEMA_ENTRY(dict(entry.data))
except vol.MultipleInvalid as ex:
raise ConfigEntryError(
f"The MQTT config entry is invalid, please correct it: {ex}"
) from ex
# Fetch configuration and add default values
conf = dict(entry.data)
# Fetch configuration
hass_config = await conf_util.async_hass_config_yaml(hass)
mqtt_yaml = PLATFORM_CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {}))
client = MQTT(hass, entry, conf)
@@ -390,7 +387,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
)
# Setup discovery
if conf.get(CONF_DISCOVERY):
if conf.get(CONF_DISCOVERY, DEFAULT_DISCOVERY):
await _async_setup_discovery(hass, conf, entry)
# Setup reload service after all platforms have loaded
await async_setup_reload_service()

View File

@@ -44,7 +44,6 @@ from homeassistant.util.async_ import run_callback_threadsafe
from homeassistant.util.logging import catch_log_exception
from .const import (
ATTR_TOPIC,
CONF_BIRTH_MESSAGE,
CONF_BROKER,
CONF_CERTIFICATE,
@@ -56,10 +55,16 @@ from .const import (
CONF_WILL_MESSAGE,
CONF_WS_HEADERS,
CONF_WS_PATH,
DEFAULT_BIRTH,
DEFAULT_ENCODING,
DEFAULT_KEEPALIVE,
DEFAULT_PORT,
DEFAULT_PROTOCOL,
DEFAULT_QOS,
DEFAULT_TRANSPORT,
DEFAULT_WILL,
DEFAULT_WS_HEADERS,
DEFAULT_WS_PATH,
MQTT_CONNECTED,
MQTT_DISCONNECTED,
PROTOCOL_5,
@@ -273,8 +278,8 @@ class MqttClientSetup:
client_cert = get_file_path(CONF_CLIENT_CERT, config.get(CONF_CLIENT_CERT))
tls_insecure = config.get(CONF_TLS_INSECURE)
if transport == TRANSPORT_WEBSOCKETS:
ws_path: str = config[CONF_WS_PATH]
ws_headers: dict[str, str] = config[CONF_WS_HEADERS]
ws_path: str = config.get(CONF_WS_PATH, DEFAULT_WS_PATH)
ws_headers: dict[str, str] = config.get(CONF_WS_HEADERS, DEFAULT_WS_HEADERS)
self._client.ws_set_options(ws_path, ws_headers)
if certificate is not None:
self._client.tls_set(
@@ -452,15 +457,8 @@ class MQTT:
self._mqttc.on_subscribe = self._mqtt_on_callback
self._mqttc.on_unsubscribe = self._mqtt_on_callback
if (
CONF_WILL_MESSAGE in self.conf
and ATTR_TOPIC in self.conf[CONF_WILL_MESSAGE]
):
will_message = PublishMessage(**self.conf[CONF_WILL_MESSAGE])
else:
will_message = None
if will_message is not None:
if will := self.conf.get(CONF_WILL_MESSAGE, DEFAULT_WILL):
will_message = PublishMessage(**will)
self._mqttc.will_set(
topic=will_message.topic,
payload=will_message.payload,
@@ -503,8 +501,8 @@ class MQTT:
result = await self.hass.async_add_executor_job(
self._mqttc.connect,
self.conf[CONF_BROKER],
self.conf[CONF_PORT],
self.conf[CONF_KEEPALIVE],
self.conf.get(CONF_PORT, DEFAULT_PORT),
self.conf.get(CONF_KEEPALIVE, DEFAULT_KEEPALIVE),
)
except OSError as err:
_LOGGER.error("Failed to connect to MQTT server due to exception: %s", err)
@@ -738,16 +736,13 @@ class MQTT:
_LOGGER.info(
"Connected to MQTT server %s:%s (%s)",
self.conf[CONF_BROKER],
self.conf[CONF_PORT],
self.conf.get(CONF_PORT, DEFAULT_PORT),
result_code,
)
self.hass.create_task(self._async_resubscribe())
if (
CONF_BIRTH_MESSAGE in self.conf
and ATTR_TOPIC in self.conf[CONF_BIRTH_MESSAGE]
):
if birth := self.conf.get(CONF_BIRTH_MESSAGE, DEFAULT_BIRTH):
async def publish_birth_message(birth_message: PublishMessage) -> None:
await self._ha_started.wait() # Wait for Home Assistant to start
@@ -761,7 +756,7 @@ class MQTT:
retain=birth_message.retain,
)
birth_message = PublishMessage(**self.conf[CONF_BIRTH_MESSAGE])
birth_message = PublishMessage(**birth)
asyncio.run_coroutine_threadsafe(
publish_birth_message(birth_message), self.hass.loop
)
@@ -880,7 +875,7 @@ class MQTT:
_LOGGER.warning(
"Disconnected from MQTT server %s:%s (%s)",
self.conf[CONF_BROKER],
self.conf[CONF_PORT],
self.conf.get(CONF_PORT, DEFAULT_PORT),
result_code,
)

View File

@@ -47,7 +47,6 @@ from homeassistant.helpers.selector import (
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
from .client import MqttClientSetup
from .config_integration import CONFIG_SCHEMA_ENTRY
from .const import (
ATTR_PAYLOAD,
ATTR_QOS,
@@ -369,7 +368,6 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
updated_config = {}
updated_config.update(self.broker_config)
updated_config.update(options_config)
CONFIG_SCHEMA_ENTRY(updated_config)
self.hass.config_entries.async_update_entry(
self.config_entry,
data=updated_config,

View File

@@ -45,23 +45,8 @@ from .const import (
CONF_DISCOVERY_PREFIX,
CONF_KEEPALIVE,
CONF_TLS_INSECURE,
CONF_TRANSPORT,
CONF_WILL_MESSAGE,
CONF_WS_HEADERS,
CONF_WS_PATH,
DEFAULT_BIRTH,
DEFAULT_DISCOVERY,
DEFAULT_KEEPALIVE,
DEFAULT_PORT,
DEFAULT_PREFIX,
DEFAULT_PROTOCOL,
DEFAULT_TRANSPORT,
DEFAULT_WILL,
SUPPORTED_PROTOCOLS,
TRANSPORT_TCP,
TRANSPORT_WEBSOCKETS,
)
from .util import valid_birth_will, valid_publish_topic
DEFAULT_TLS_PROTOCOL = "auto"
@@ -155,41 +140,6 @@ CLIENT_KEY_AUTH_MSG = (
"client_key and client_cert must both be present in the MQTT broker configuration"
)
CONFIG_SCHEMA_ENTRY = vol.Schema(
{
vol.Optional(CONF_CLIENT_ID): cv.string,
vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All(
vol.Coerce(int), vol.Range(min=15)
),
vol.Required(CONF_BROKER): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_CERTIFICATE): str,
vol.Inclusive(CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG): str,
vol.Inclusive(
CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG
): str,
vol.Optional(CONF_TLS_INSECURE): cv.boolean,
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(
cv.string, vol.In(SUPPORTED_PROTOCOLS)
),
vol.Optional(CONF_WILL_MESSAGE, default=DEFAULT_WILL): valid_birth_will,
vol.Optional(CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH): valid_birth_will,
vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean,
# discovery_prefix must be a valid publish topic because if no
# state topic is specified, it will be created with the given prefix.
vol.Optional(
CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX
): valid_publish_topic,
vol.Optional(CONF_TRANSPORT, default=DEFAULT_TRANSPORT): vol.All(
cv.string, vol.In([TRANSPORT_TCP, TRANSPORT_WEBSOCKETS])
),
vol.Optional(CONF_WS_PATH, default="/"): cv.string,
vol.Optional(CONF_WS_HEADERS, default={}): {cv.string: cv.string},
}
)
DEPRECATED_CONFIG_KEYS = [
CONF_BIRTH_MESSAGE,
CONF_BROKER,

View File

@@ -46,6 +46,7 @@ DEFAULT_PAYLOAD_AVAILABLE = "online"
DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline"
DEFAULT_PORT = 1883
DEFAULT_RETAIN = False
DEFAULT_WS_HEADERS: dict[str, str] = {}
DEFAULT_WS_PATH = "/"
PROTOCOL_31 = "3.1"

View File

@@ -25,7 +25,6 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
@@ -47,7 +46,7 @@ from .const import (
DOMAIN,
ERROR_STATES,
)
from .helpers import parse_id
from .helpers import NukiWebhookException, parse_id
_NukiDeviceT = TypeVar("_NukiDeviceT", bound=NukiDevice)
@@ -61,6 +60,87 @@ def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOp
return bridge.locks, bridge.openers
async def _create_webhook(
hass: HomeAssistant, entry: ConfigEntry, bridge: NukiBridge
) -> None:
# Create HomeAssistant webhook
async def handle_webhook(
hass: HomeAssistant, webhook_id: str, request: web.Request
) -> web.Response:
"""Handle webhook callback."""
try:
data = await request.json()
except ValueError:
return web.Response(status=HTTPStatus.BAD_REQUEST)
locks = hass.data[DOMAIN][entry.entry_id][DATA_LOCKS]
openers = hass.data[DOMAIN][entry.entry_id][DATA_OPENERS]
devices = [x for x in locks + openers if x.nuki_id == data["nukiId"]]
if len(devices) == 1:
devices[0].update_from_callback(data)
coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
coordinator.async_set_updated_data(None)
return web.Response(status=HTTPStatus.OK)
webhook.async_register(
hass, DOMAIN, entry.title, entry.entry_id, handle_webhook, local_only=True
)
webhook_url = webhook.async_generate_path(entry.entry_id)
try:
hass_url = get_url(
hass,
allow_cloud=False,
allow_external=False,
allow_ip=True,
require_ssl=False,
)
except NoURLAvailableError:
webhook.async_unregister(hass, entry.entry_id)
raise NukiWebhookException(
f"Error registering URL for webhook {entry.entry_id}: "
"HomeAssistant URL is not available"
) from None
url = f"{hass_url}{webhook_url}"
if hass_url.startswith("https"):
ir.async_create_issue(
hass,
DOMAIN,
"https_webhook",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="https_webhook",
translation_placeholders={
"base_url": hass_url,
"network_link": "https://my.home-assistant.io/redirect/network/",
},
)
else:
ir.async_delete_issue(hass, DOMAIN, "https_webhook")
try:
async with async_timeout.timeout(10):
await hass.async_add_executor_job(
_register_webhook, bridge, entry.entry_id, url
)
except InvalidCredentialsException as err:
webhook.async_unregister(hass, entry.entry_id)
raise NukiWebhookException(
f"Invalid credentials for Bridge: {err}"
) from err
except RequestException as err:
webhook.async_unregister(hass, entry.entry_id)
raise NukiWebhookException(
f"Error communicating with Bridge: {err}"
) from err
def _register_webhook(bridge: NukiBridge, entry_id: str, url: str) -> bool:
# Register HA URL as webhook if not already
callbacks = bridge.callback_list()
@@ -126,79 +206,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
sw_version=info["versions"]["firmwareVersion"],
)
async def handle_webhook(
hass: HomeAssistant, webhook_id: str, request: web.Request
) -> web.Response:
"""Handle webhook callback."""
try:
data = await request.json()
except ValueError:
return web.Response(status=HTTPStatus.BAD_REQUEST)
locks = hass.data[DOMAIN][entry.entry_id][DATA_LOCKS]
openers = hass.data[DOMAIN][entry.entry_id][DATA_OPENERS]
devices = [x for x in locks + openers if x.nuki_id == data["nukiId"]]
if len(devices) == 1:
devices[0].update_from_callback(data)
coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
coordinator.async_set_updated_data(None)
return web.Response(status=HTTPStatus.OK)
webhook.async_register(
hass, DOMAIN, entry.title, entry.entry_id, handle_webhook, local_only=True
)
webhook_url = webhook.async_generate_path(entry.entry_id)
try:
hass_url = get_url(
hass,
allow_cloud=False,
allow_external=False,
allow_ip=True,
require_ssl=False,
)
except NoURLAvailableError:
webhook.async_unregister(hass, entry.entry_id)
raise ConfigEntryNotReady(
f"Error registering URL for webhook {entry.entry_id}: "
"HomeAssistant URL is not available"
) from None
url = f"{hass_url}{webhook_url}"
if hass_url.startswith("https"):
ir.async_create_issue(
hass,
DOMAIN,
"https_webhook",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="https_webhook",
translation_placeholders={
"base_url": hass_url,
"network_link": "https://my.home-assistant.io/redirect/network/",
},
)
else:
ir.async_delete_issue(hass, DOMAIN, "https_webhook")
try:
async with async_timeout.timeout(10):
await hass.async_add_executor_job(
_register_webhook, bridge, entry.entry_id, url
)
except InvalidCredentialsException as err:
webhook.async_unregister(hass, entry.entry_id)
raise ConfigEntryNotReady(f"Invalid credentials for Bridge: {err}") from err
except RequestException as err:
webhook.async_unregister(hass, entry.entry_id)
raise ConfigEntryNotReady(
f"Error communicating with Bridge: {err}"
) from err
await _create_webhook(hass, entry, bridge)
except NukiWebhookException as err:
_LOGGER.warning("Error registering HomeAssistant webhook: %s", err)
async def _stop_nuki(_: Event):
"""Stop and remove the Nuki webhook."""

View File

@@ -13,3 +13,7 @@ class CannotConnect(exceptions.HomeAssistantError):
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""
class NukiWebhookException(exceptions.HomeAssistantError):
"""Error to indicate there was an issue with the webhook."""

View File

@@ -3,5 +3,5 @@
"name": "Camera Proxy",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/proxy",
"requirements": ["pillow==9.4.0"]
"requirements": ["pillow==9.5.0"]
}

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/qrcode",
"iot_class": "calculated",
"loggers": ["pyzbar"],
"requirements": ["pillow==9.4.0", "pyzbar==0.1.7"]
"requirements": ["pillow==9.5.0", "pyzbar==0.1.7"]
}

View File

@@ -4,5 +4,5 @@
"codeowners": ["@fabaff"],
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
"iot_class": "local_polling",
"requirements": ["pillow==9.4.0"]
"requirements": ["pillow==9.5.0"]
}

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/sighthound",
"iot_class": "cloud_polling",
"loggers": ["simplehound"],
"requirements": ["pillow==9.4.0", "simplehound==0.3"]
"requirements": ["pillow==9.5.0", "simplehound==0.3"]
}

View File

@@ -9,6 +9,6 @@
"iot_class": "cloud_polling",
"loggers": ["spotipy"],
"quality_scale": "silver",
"requirements": ["spotipy==2.22.1"],
"requirements": ["spotipy==2.23.0"],
"zeroconf": ["_spotify-connect._tcp.local."]
}

View File

@@ -3,7 +3,11 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.cover import CoverEntity, CoverEntityFeature
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
CoverEntity,
CoverEntityFeature,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -36,6 +40,7 @@ async def async_setup_entry(
CoverSwitch(
hass,
config_entry.title,
COVER_DOMAIN,
entity_id,
config_entry.entry_id,
)

View File

@@ -23,13 +23,15 @@ class BaseEntity(Entity):
"""Represents a Switch as an X."""
_attr_should_poll = False
_is_new_entity: bool
def __init__(
self,
hass: HomeAssistant,
config_entry_title: str,
domain: str,
switch_entity_id: str,
unique_id: str | None,
unique_id: str,
) -> None:
"""Initialize Switch as an X."""
registry = er.async_get(hass)
@@ -41,7 +43,7 @@ class BaseEntity(Entity):
name: str | None = config_entry_title
if wrapped_switch:
name = wrapped_switch.name or wrapped_switch.original_name
name = wrapped_switch.original_name
self._device_id = device_id
if device_id and (device := device_registry.async_get(device_id)):
@@ -55,6 +57,10 @@ class BaseEntity(Entity):
self._attr_unique_id = unique_id
self._switch_entity_id = switch_entity_id
self._is_new_entity = (
registry.async_get_entity_id(domain, SWITCH_AS_X_DOMAIN, unique_id) is None
)
@callback
def async_state_changed_listener(self, event: Event | None = None) -> None:
"""Handle child updates."""
@@ -67,7 +73,7 @@ class BaseEntity(Entity):
self._attr_available = True
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
"""Register callbacks and copy the wrapped entity's custom name if set."""
@callback
def _async_state_changed_listener(event: Event | None = None) -> None:
@@ -93,6 +99,15 @@ class BaseEntity(Entity):
{"entity_id": self._switch_entity_id},
)
if not self._is_new_entity:
return
wrapped_switch = registry.async_get(self._switch_entity_id)
if not wrapped_switch or wrapped_switch.name is None:
return
registry.async_update_entity(self.entity_id, name=wrapped_switch.name)
class BaseToggleEntity(BaseEntity, ToggleEntity):
"""Represents a Switch as a ToggleEntity."""

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.fan import FanEntity
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN, FanEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.core import HomeAssistant
@@ -29,6 +29,7 @@ async def async_setup_entry(
FanSwitch(
hass,
config_entry.title,
FAN_DOMAIN,
entity_id,
config_entry.entry_id,
)

View File

@@ -1,7 +1,11 @@
"""Light support for switch entities."""
from __future__ import annotations
from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.components.light import (
DOMAIN as LIGHT_DOMAIN,
ColorMode,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.core import HomeAssistant
@@ -27,6 +31,7 @@ async def async_setup_entry(
LightSwitch(
hass,
config_entry.title,
LIGHT_DOMAIN,
entity_id,
config_entry.entry_id,
)

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.lock import LockEntity
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -36,6 +36,7 @@ async def async_setup_entry(
LockSwitch(
hass,
config_entry.title,
LOCK_DOMAIN,
entity_id,
config_entry.entry_id,
)

View File

@@ -1,7 +1,11 @@
"""Siren support for switch entities."""
from __future__ import annotations
from homeassistant.components.siren import SirenEntity, SirenEntityFeature
from homeassistant.components.siren import (
DOMAIN as SIREN_DOMAIN,
SirenEntity,
SirenEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.core import HomeAssistant
@@ -27,6 +31,7 @@ async def async_setup_entry(
SirenSwitch(
hass,
config_entry.title,
SIREN_DOMAIN,
entity_id,
config_entry.entry_id,
)

View File

@@ -7,5 +7,5 @@
"iot_class": "local_push",
"loggers": ["aioswitcher"],
"quality_scale": "platinum",
"requirements": ["aioswitcher==3.2.1"]
"requirements": ["aioswitcher==3.3.0"]
}

View File

@@ -10,6 +10,6 @@
"tf-models-official==2.5.0",
"pycocotools==2.0.1",
"numpy==1.23.2",
"pillow==9.4.0"
"pillow==9.5.0"
]
}

View File

@@ -54,7 +54,6 @@ CLIENT_CONNECTED_ATTRIBUTES = [
]
CLIENT_STATIC_ATTRIBUTES = [
"hostname",
"mac",
"name",
"oui",
@@ -175,7 +174,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiTrackerEntityDescription, ...] = (
supported_fn=lambda controller, obj_id: True,
unique_id_fn=lambda controller, obj_id: f"{obj_id}-{controller.site}",
ip_address_fn=lambda api, obj_id: api.clients[obj_id].ip,
hostname_fn=lambda api, obj_id: None,
hostname_fn=lambda api, obj_id: api.clients[obj_id].hostname,
),
UnifiTrackerEntityDescription[Devices, Device](
key="Device scanner",

View File

@@ -32,7 +32,7 @@ class TemplateError(HomeAssistantError):
super().__init__(f"{exception.__class__.__name__}: {exception}")
@dataclass
@dataclass(slots=True)
class ConditionError(HomeAssistantError):
"""Error during condition evaluation."""
@@ -52,7 +52,7 @@ class ConditionError(HomeAssistantError):
return "\n".join(list(self.output(indent=0)))
@dataclass
@dataclass(slots=True)
class ConditionErrorMessage(ConditionError):
"""Condition error message."""
@@ -64,7 +64,7 @@ class ConditionErrorMessage(ConditionError):
yield self._indent(indent, f"In '{self.type}' condition: {self.message}")
@dataclass
@dataclass(slots=True)
class ConditionErrorIndex(ConditionError):
"""Condition error with index."""
@@ -87,7 +87,7 @@ class ConditionErrorIndex(ConditionError):
yield from self.error.output(indent + 1)
@dataclass
@dataclass(slots=True)
class ConditionErrorContainer(ConditionError):
"""Condition error with subconditions."""

View File

@@ -35,7 +35,7 @@ CHANGE_REMOVED = "removed"
_T = TypeVar("_T")
@dataclass
@dataclass(slots=True)
class CollectionChangeSet:
"""Class to represent a change set.

View File

@@ -205,7 +205,7 @@ class EntityPlatformState(Enum):
REMOVED = auto()
@dataclass
@dataclass(slots=True)
class EntityDescription:
"""A class that describes Home Assistant entities."""
@@ -981,7 +981,7 @@ class Entity(ABC):
return report_issue
@dataclass
@dataclass(slots=True)
class ToggleEntityDescription(EntityDescription):
"""A class that describes toggle entities."""

View File

@@ -66,7 +66,7 @@ RANDOM_MICROSECOND_MAX = 500000
_P = ParamSpec("_P")
@dataclass
@dataclass(slots=True)
class TrackStates:
"""Class for keeping track of states being tracked.
@@ -80,7 +80,7 @@ class TrackStates:
domains: set[str]
@dataclass
@dataclass(slots=True)
class TrackTemplate:
"""Class for keeping track of a template with variables.
@@ -94,7 +94,7 @@ class TrackTemplate:
rate_limit: timedelta | None = None
@dataclass
@dataclass(slots=True)
class TrackTemplateResult:
"""Class for result of template tracking.

View File

@@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
DATA_INTEGRATION_PLATFORMS = "integration_platforms"
@dataclass(frozen=True)
@dataclass(slots=True, frozen=True)
class IntegrationPlatform:
"""An integration platform."""

View File

@@ -568,7 +568,7 @@ class IntentResponseTargetType(str, Enum):
CUSTOM = "custom"
@dataclass
@dataclass(slots=True)
class IntentResponseTarget:
"""Target of the intent response."""

View File

@@ -32,7 +32,7 @@ class IssueSeverity(StrEnum):
WARNING = "warning"
@dataclasses.dataclass(frozen=True)
@dataclasses.dataclass(slots=True, frozen=True)
class IssueEntry:
"""Issue Registry Entry."""

View File

@@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant, callback
DOMAIN = "recorder"
@dataclass
@dataclass(slots=True)
class RecorderData:
"""Recorder data stored in hass.data."""

View File

@@ -27,7 +27,7 @@ class SchemaFlowStep:
"""Define a config or options flow step."""
@dataclass
@dataclass(slots=True)
class SchemaFlowFormStep(SchemaFlowStep):
"""Define a config or options flow form step."""
@@ -79,7 +79,7 @@ class SchemaFlowFormStep(SchemaFlowStep):
"""
@dataclass
@dataclass(slots=True)
class SchemaFlowMenuStep(SchemaFlowStep):
"""Define a config or options flow menu step."""

View File

@@ -199,7 +199,7 @@ class ServiceTargetSelector:
return bool(self.entity_ids or self.device_ids or self.area_ids)
@dataclasses.dataclass
@dataclasses.dataclass(slots=True)
class SelectedEntities:
"""Class to hold the selected entities."""

View File

@@ -7,7 +7,7 @@ from homeassistant.data_entry_flow import BaseServiceInfo
ReceivePayloadType = str | bytes
@dataclass
@dataclass(slots=True)
class MqttServiceInfo(BaseServiceInfo):
"""Prepared info from mqtt entries."""

View File

@@ -95,7 +95,7 @@ class TriggerInfo(TypedDict):
trigger_data: TriggerData
@dataclass
@dataclass(slots=True)
class PluggableActionsEntry:
"""Holder to keep track of all plugs and actions for a given trigger."""

View File

@@ -123,7 +123,7 @@ class USBMatcher(USBMatcherRequired, USBMatcherOptional):
"""Matcher for the bluetooth integration."""
@dataclass
@dataclass(slots=True)
class HomeKitDiscoveredIntegration:
"""HomeKit model."""

View File

@@ -25,7 +25,7 @@ ha-av==10.0.0
hass-nabucasa==0.63.1
hassil==1.0.6
home-assistant-bluetooth==1.9.3
home-assistant-frontend==20230406.1
home-assistant-frontend==20230411.0
home-assistant-intents==2023.3.29
httpx==0.23.3
ifaddr==0.1.7
@@ -34,7 +34,7 @@ jinja2==3.1.2
lru-dict==1.1.8
orjson==3.8.10
paho-mqtt==1.6.1
pillow==9.4.0
pillow==9.5.0
pip>=21.0,<23.1
psutil-home-assistant==0.0.1
pyOpenSSL==23.1.0

View File

@@ -34,7 +34,7 @@ ALPINE_RELEASE_FILE = "/etc/alpine-release"
_LOGGER = logging.getLogger(__name__)
@dataclasses.dataclass
@dataclasses.dataclass(slots=True)
class RuntimeConfig:
"""Class to hold the information for running Home Assistant."""

View File

@@ -18,7 +18,7 @@ class NodeDictClass(dict):
"""Wrapper class to be able to add attributes on a dict."""
@dataclass(frozen=True)
@dataclass(slots=True, frozen=True)
class Input:
"""Input that should be substituted."""

View File

@@ -282,7 +282,7 @@ aiosomecomfort==0.0.14
aiosteamist==0.3.2
# homeassistant.components.switcher_kis
aioswitcher==3.2.1
aioswitcher==3.3.0
# homeassistant.components.syncthing
aiosyncthing==0.5.1
@@ -907,7 +907,7 @@ hole==0.8.0
holidays==0.21.13
# homeassistant.components.frontend
home-assistant-frontend==20230406.1
home-assistant-frontend==20230411.0
# homeassistant.components.conversation
home-assistant-intents==2023.3.29
@@ -1355,7 +1355,7 @@ pilight==0.1.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
pillow==9.4.0
pillow==9.5.0
# homeassistant.components.dominos
pizzapi==0.0.3
@@ -2388,7 +2388,7 @@ speedtest-cli==2.1.3
spiderpy==1.6.1
# homeassistant.components.spotify
spotipy==2.22.1
spotipy==2.23.0
# homeassistant.components.recorder
# homeassistant.components.sql

View File

@@ -263,7 +263,7 @@ aiosomecomfort==0.0.14
aiosteamist==0.3.2
# homeassistant.components.switcher_kis
aioswitcher==3.2.1
aioswitcher==3.3.0
# homeassistant.components.syncthing
aiosyncthing==0.5.1
@@ -693,7 +693,7 @@ hole==0.8.0
holidays==0.21.13
# homeassistant.components.frontend
home-assistant-frontend==20230406.1
home-assistant-frontend==20230411.0
# homeassistant.components.conversation
home-assistant-intents==2023.3.29
@@ -997,7 +997,7 @@ pilight==0.1.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
pillow==9.4.0
pillow==9.5.0
# homeassistant.components.plex
plexapi==4.13.2
@@ -1709,7 +1709,7 @@ speedtest-cli==2.1.3
spiderpy==1.6.1
# homeassistant.components.spotify
spotipy==2.22.1
spotipy==2.23.0
# homeassistant.components.recorder
# homeassistant.components.sql

View File

@@ -148,7 +148,7 @@ async def test_http_processing_intent_target_ha_agent(
}
async def test_http_processing_intent_entity_added(
async def test_http_processing_intent_entity_added_removed(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
@@ -198,7 +198,7 @@ async def test_http_processing_intent_entity_added(
"conversation_id": None,
}
# Add an alias
# Add an entity
entity_registry.async_get_or_create(
"light", "demo", "5678", suggested_object_id="late"
)
@@ -294,6 +294,288 @@ async def test_http_processing_intent_entity_added(
}
async def test_http_processing_intent_alias_added_removed(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
entity_registry: er.EntityRegistry,
) -> None:
"""Test processing intent via HTTP API with aliases added later.
We want to ensure that adding an alias later busts the cache
so that the new alias is available.
"""
entity_registry.async_get_or_create(
"light", "demo", "1234", suggested_object_id="kitchen"
)
hass.states.async_set("light.kitchen", "off", {"friendly_name": "kitchen light"})
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
assert len(calls) == 1
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
# Add an alias
entity_registry.async_update_entity("light.kitchen", aliases={"late added alias"})
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on late added alias"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
# Now remove the alieas
entity_registry.async_update_entity("light.kitchen", aliases={})
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on late added alias"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"conversation_id": None,
"response": {
"card": {},
"data": {"code": "no_intent_match"},
"language": hass.config.language,
"response_type": "error",
"speech": {
"plain": {
"extra_data": None,
"speech": "Sorry, I couldn't understand that",
}
},
},
}
async def test_http_processing_intent_entity_renamed(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
entity_registry: er.EntityRegistry,
enable_custom_integrations: None,
) -> None:
"""Test processing intent via HTTP API with entities renamed later.
We want to ensure that renaming an entity later busts the cache
so that the new name is used.
"""
platform = getattr(hass.components, "test.light")
platform.init(empty=True)
entity = platform.MockLight("kitchen light", "on")
entity._attr_unique_id = "1234"
entity.entity_id = "light.kitchen"
platform.ENTITIES.append(entity)
assert await async_setup_component(
hass,
LIGHT_DOMAIN,
{LIGHT_DOMAIN: [{"platform": "test"}]},
)
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
assert len(calls) == 1
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
# Rename the entity
entity_registry.async_update_entity("light.kitchen", name="renamed light")
await hass.async_block_till_done()
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on renamed light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "renamed light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"conversation_id": None,
"response": {
"card": {},
"data": {"code": "no_intent_match"},
"language": hass.config.language,
"response_type": "error",
"speech": {
"plain": {
"extra_data": None,
"speech": "Sorry, I couldn't understand that",
}
},
},
}
# Now clear the custom name
entity_registry.async_update_entity("light.kitchen", name=None)
await hass.async_block_till_done()
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on kitchen light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
client = await hass_client()
resp = await client.post(
"/api/conversation/process", json={"text": "turn on renamed light"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"conversation_id": None,
"response": {
"card": {},
"data": {"code": "no_intent_match"},
"language": hass.config.language,
"response_type": "error",
"speech": {
"plain": {
"extra_data": None,
"speech": "Sorry, I couldn't understand that",
}
},
},
}
@pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS)
@pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on"))
async def test_turn_on_intent(

View File

@@ -19,20 +19,6 @@ from tests.typing import ClientSessionGenerator, MqttMockHAClientGenerator
default_config = {
"birth_message": {},
"broker": "mock-broker",
"discovery": True,
"discovery_prefix": "homeassistant",
"keepalive": 60,
"port": 1883,
"protocol": "3.1.1",
"transport": "tcp",
"will_message": {
"payload": "offline",
"qos": 0,
"retain": False,
"topic": "homeassistant/status",
},
"ws_headers": {},
"ws_path": "/",
}
@@ -57,6 +43,7 @@ async def test_entry_diagnostics(
config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
mqtt_mock.connected = True
await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {
"connected": True,
"devices": [],

View File

@@ -2288,23 +2288,6 @@ async def test_default_entry_setting_are_applied(
assert device_entry is not None
async def test_fail_no_broker(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mqtt_client_mock: MqttMockPahoClient,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the MQTT entry setup when broker configuration is missing."""
# Config entry data is incomplete
entry = MockConfigEntry(domain=mqtt.DOMAIN, data={})
entry.add_to_hass(hass)
assert not await hass.config_entries.async_setup(entry.entry_id)
assert (
"The MQTT config entry is invalid, please correct it: required key not provided @ data['broker']"
in caplog.text
)
@pytest.mark.no_fail_on_log_exception
async def test_message_callback_exception_gets_logged(
hass: HomeAssistant,
@@ -3312,41 +3295,16 @@ async def test_setup_manual_items_with_unique_ids(
assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique
async def test_fail_with_unknown_conf_entry_options(
hass: HomeAssistant,
mqtt_client_mock: MqttMockPahoClient,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test unknown keys in config entry data is removed."""
mqtt_config_entry_data = {
mqtt.CONF_BROKER: "mock-broker",
mqtt.CONF_BIRTH_MESSAGE: {},
"old_option": "old_value",
}
entry = MockConfigEntry(
data=mqtt_config_entry_data,
domain=mqtt.DOMAIN,
title="MQTT",
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id) is False
assert ("extra keys not allowed @ data['old_option']") in caplog.text
@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT])
@pytest.mark.parametrize(
"hass_config",
[
{
"mqtt": {
"light": [
"sensor": [
{
"name": "test_manual",
"unique_id": "test_manual_unique_id123",
"command_topic": "test-topic_manual",
"state_topic": "test-topic_manual",
}
]
}
@@ -3366,15 +3324,16 @@ async def test_link_config_entry(
config_discovery = {
"name": "test_discovery",
"unique_id": "test_discovery_unique456",
"command_topic": "test-topic_discovery",
"state_topic": "test-topic_discovery",
}
async_fire_mqtt_message(
hass, "homeassistant/light/bla/config", json.dumps(config_discovery)
hass, "homeassistant/sensor/bla/config", json.dumps(config_discovery)
)
await hass.async_block_till_done()
await hass.async_block_till_done()
assert hass.states.get("light.test_manual") is not None
assert hass.states.get("light.test_discovery") is not None
assert hass.states.get("sensor.test_manual") is not None
assert hass.states.get("sensor.test_discovery") is not None
entity_names = ["test_manual", "test_discovery"]
# Check if both entities were linked to the MQTT config entry
@@ -3402,7 +3361,7 @@ async def test_link_config_entry(
assert _check_entities() == 1
# set up item through discovery
async_fire_mqtt_message(
hass, "homeassistant/light/bla/config", json.dumps(config_discovery)
hass, "homeassistant/sensor/bla/config", json.dumps(config_discovery)
)
await hass.async_block_till_done()
assert _check_entities() == 2

View File

@@ -534,7 +534,143 @@ async def test_entity_name(
assert entity_entry
assert entity_entry.device_id == switch_entity_entry.device_id
assert entity_entry.has_entity_name is True
assert entity_entry.name is None
assert entity_entry.original_name is None
assert entity_entry.options == {
DOMAIN: {"entity_id": switch_entity_entry.entity_id}
}
@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST)
async def test_custom_name_1(
hass: HomeAssistant,
target_domain: Platform,
) -> None:
"""Test the source entity has a custom name."""
registry = er.async_get(hass)
device_registry = dr.async_get(hass)
switch_config_entry = MockConfigEntry()
device_entry = device_registry.async_get_or_create(
config_entry_id=switch_config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
name="Device name",
)
switch_entity_entry = registry.async_get_or_create(
"switch",
"test",
"unique",
device_id=device_entry.id,
has_entity_name=True,
original_name="Original entity name",
)
switch_entity_entry = registry.async_update_entity(
switch_entity_entry.entity_id,
config_entry_id=switch_config_entry.entry_id,
name="Custom entity name",
)
# Add the config entry
switch_as_x_config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={
CONF_ENTITY_ID: switch_entity_entry.id,
CONF_TARGET_DOMAIN: target_domain,
},
title="ABC",
)
switch_as_x_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id)
await hass.async_block_till_done()
entity_entry = registry.async_get(
f"{target_domain}.device_name_original_entity_name"
)
assert entity_entry
assert entity_entry.device_id == switch_entity_entry.device_id
assert entity_entry.has_entity_name is True
assert entity_entry.name == "Custom entity name"
assert entity_entry.original_name == "Original entity name"
assert entity_entry.options == {
DOMAIN: {"entity_id": switch_entity_entry.entity_id}
}
@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST)
async def test_custom_name_2(
hass: HomeAssistant,
target_domain: Platform,
) -> None:
"""Test the source entity has a custom name.
This tests the custom name is only copied from the source device when the config
switch_as_x config entry is setup the first time.
"""
registry = er.async_get(hass)
device_registry = dr.async_get(hass)
switch_config_entry = MockConfigEntry()
device_entry = device_registry.async_get_or_create(
config_entry_id=switch_config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
name="Device name",
)
switch_entity_entry = registry.async_get_or_create(
"switch",
"test",
"unique",
device_id=device_entry.id,
has_entity_name=True,
original_name="Original entity name",
)
switch_entity_entry = registry.async_update_entity(
switch_entity_entry.entity_id,
config_entry_id=switch_config_entry.entry_id,
name="New custom entity name",
)
# Add the config entry
switch_as_x_config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={
CONF_ENTITY_ID: switch_entity_entry.id,
CONF_TARGET_DOMAIN: target_domain,
},
title="ABC",
)
switch_as_x_config_entry.add_to_hass(hass)
switch_as_x_entity_entry = registry.async_get_or_create(
target_domain,
"switch_as_x",
switch_as_x_config_entry.entry_id,
suggested_object_id="device_name_original_entity_name",
)
switch_as_x_entity_entry = registry.async_update_entity(
switch_as_x_entity_entry.entity_id,
config_entry_id=switch_config_entry.entry_id,
name="Old custom entity name",
)
assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id)
await hass.async_block_till_done()
entity_entry = registry.async_get(
f"{target_domain}.device_name_original_entity_name"
)
assert entity_entry
assert entity_entry.entity_id == switch_as_x_entity_entry.entity_id
assert entity_entry.device_id == switch_entity_entry.device_id
assert entity_entry.has_entity_name is True
assert entity_entry.name == "Old custom entity name"
assert entity_entry.original_name == "Original entity name"
assert entity_entry.options == {
DOMAIN: {"entity_id": switch_entity_entry.entity_id}
}

View File

@@ -144,6 +144,9 @@ async def test_tracked_clients(
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 4
assert hass.states.get("device_tracker.client_1").state == STATE_NOT_HOME
assert hass.states.get("device_tracker.client_2").state == STATE_NOT_HOME
assert (
hass.states.get("device_tracker.client_5").attributes["host_name"] == "client_5"
)
# Client on SSID not in SSID filter
assert not hass.states.get("device_tracker.client_3")