mirror of
https://github.com/home-assistant/core.git
synced 2025-08-03 20:55:10 +02:00
Merge branch 'dev' into single_history_query
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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,
|
||||
)
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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"
|
||||
|
@@ -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."""
|
||||
|
@@ -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."""
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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."]
|
||||
}
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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."""
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioswitcher"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioswitcher==3.2.1"]
|
||||
"requirements": ["aioswitcher==3.3.0"]
|
||||
}
|
||||
|
@@ -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"
|
||||
]
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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."""
|
||||
|
||||
|
@@ -35,7 +35,7 @@ CHANGE_REMOVED = "removed"
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True)
|
||||
class CollectionChangeSet:
|
||||
"""Class to represent a change set.
|
||||
|
||||
|
@@ -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."""
|
||||
|
||||
|
@@ -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.
|
||||
|
||||
|
@@ -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."""
|
||||
|
||||
|
@@ -568,7 +568,7 @@ class IntentResponseTargetType(str, Enum):
|
||||
CUSTOM = "custom"
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True)
|
||||
class IntentResponseTarget:
|
||||
"""Target of the intent response."""
|
||||
|
||||
|
@@ -32,7 +32,7 @@ class IssueSeverity(StrEnum):
|
||||
WARNING = "warning"
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@dataclasses.dataclass(slots=True, frozen=True)
|
||||
class IssueEntry:
|
||||
"""Issue Registry Entry."""
|
||||
|
||||
|
@@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
DOMAIN = "recorder"
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True)
|
||||
class RecorderData:
|
||||
"""Recorder data stored in hass.data."""
|
||||
|
||||
|
@@ -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."""
|
||||
|
||||
|
@@ -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."""
|
||||
|
||||
|
@@ -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."""
|
||||
|
||||
|
@@ -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."""
|
||||
|
||||
|
@@ -123,7 +123,7 @@ class USBMatcher(USBMatcherRequired, USBMatcherOptional):
|
||||
"""Matcher for the bluetooth integration."""
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True)
|
||||
class HomeKitDiscoveredIntegration:
|
||||
"""HomeKit model."""
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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."""
|
||||
|
||||
|
@@ -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."""
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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(
|
||||
|
@@ -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": [],
|
||||
|
@@ -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
|
||||
|
@@ -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}
|
||||
}
|
||||
|
@@ -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")
|
||||
|
Reference in New Issue
Block a user