mirror of
https://github.com/home-assistant/core.git
synced 2026-04-13 13:16:15 +02:00
Compare commits
20 Commits
frenck-202
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a3051718a | ||
|
|
95c3624b01 | ||
|
|
f53b629dfd | ||
|
|
d901541f48 | ||
|
|
cdcf810506 | ||
|
|
274146cbb2 | ||
|
|
b8cdd8dccc | ||
|
|
5abaa2ae72 | ||
|
|
4a511a3e53 | ||
|
|
81a657ab2c | ||
|
|
e9a79ee0e5 | ||
|
|
ffd439abc5 | ||
|
|
982a2b8af7 | ||
|
|
ef589f9b46 | ||
|
|
81f8319af4 | ||
|
|
a061e47bec | ||
|
|
e5c49b6455 | ||
|
|
5c51820869 | ||
|
|
eb64589115 | ||
|
|
4ebf0bf0b6 |
22
CODEOWNERS
generated
22
CODEOWNERS
generated
@@ -489,8 +489,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||
/homeassistant/components/ephember/ @ttroy50 @roberty99
|
||||
/homeassistant/components/epic_games_store/ @hacf-fr @Quentame
|
||||
/tests/components/epic_games_store/ @hacf-fr @Quentame
|
||||
/homeassistant/components/epic_games_store/ @Quentame
|
||||
/tests/components/epic_games_store/ @Quentame
|
||||
/homeassistant/components/epion/ @lhgravendeel
|
||||
/tests/components/epion/ @lhgravendeel
|
||||
/homeassistant/components/epson/ @pszafer
|
||||
@@ -566,8 +566,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/fortios/ @kimfrellsen
|
||||
/homeassistant/components/foscam/ @Foscam-wangzhengyu
|
||||
/tests/components/foscam/ @Foscam-wangzhengyu
|
||||
/homeassistant/components/freebox/ @hacf-fr @Quentame
|
||||
/tests/components/freebox/ @hacf-fr @Quentame
|
||||
/homeassistant/components/freebox/ @hacf-fr/reviewers @Quentame
|
||||
/tests/components/freebox/ @hacf-fr/reviewers @Quentame
|
||||
/homeassistant/components/freedompro/ @stefano055415
|
||||
/tests/components/freedompro/ @stefano055415
|
||||
/homeassistant/components/freshr/ @SierraNL
|
||||
@@ -1057,8 +1057,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/met/ @danielhiversen
|
||||
/homeassistant/components/met_eireann/ @DylanGore
|
||||
/tests/components/met_eireann/ @DylanGore
|
||||
/homeassistant/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
||||
/tests/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
||||
/homeassistant/components/meteo_france/ @hacf-fr/reviewers @oncleben31 @Quentame
|
||||
/tests/components/meteo_france/ @hacf-fr/reviewers @oncleben31 @Quentame
|
||||
/homeassistant/components/meteo_lt/ @xE1H
|
||||
/tests/components/meteo_lt/ @xE1H
|
||||
/homeassistant/components/meteoalarm/ @rolfberkenbosch
|
||||
@@ -1150,8 +1150,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/netatmo/ @cgtobi
|
||||
/tests/components/netatmo/ @cgtobi
|
||||
/homeassistant/components/netdata/ @fabaff
|
||||
/homeassistant/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
||||
/tests/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
||||
/homeassistant/components/netgear/ @Quentame @starkillerOG
|
||||
/tests/components/netgear/ @Quentame @starkillerOG
|
||||
/homeassistant/components/netgear_lte/ @tkdrob
|
||||
/tests/components/netgear_lte/ @tkdrob
|
||||
/homeassistant/components/network/ @home-assistant/core
|
||||
@@ -1694,8 +1694,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/syncthing/ @zhulik
|
||||
/homeassistant/components/syncthru/ @nielstron
|
||||
/tests/components/syncthru/ @nielstron
|
||||
/homeassistant/components/synology_dsm/ @hacf-fr @Quentame @mib1185
|
||||
/tests/components/synology_dsm/ @hacf-fr @Quentame @mib1185
|
||||
/homeassistant/components/synology_dsm/ @Quentame @mib1185
|
||||
/tests/components/synology_dsm/ @Quentame @mib1185
|
||||
/homeassistant/components/synology_srm/ @aerialls
|
||||
/homeassistant/components/system_bridge/ @timmo001
|
||||
/tests/components/system_bridge/ @timmo001
|
||||
@@ -1828,6 +1828,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/unifi_access/ @imhotep @RaHehl
|
||||
/tests/components/unifi_access/ @imhotep @RaHehl
|
||||
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
||||
/homeassistant/components/unifi_discovery/ @RaHehl
|
||||
/tests/components/unifi_discovery/ @RaHehl
|
||||
/homeassistant/components/unifiled/ @florisvdk
|
||||
/homeassistant/components/unifiprotect/ @RaHehl
|
||||
/tests/components/unifiprotect/ @RaHehl
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"unifi",
|
||||
"unifi_access",
|
||||
"unifi_direct",
|
||||
"unifi_discovery",
|
||||
"unifiled",
|
||||
"unifiprotect"
|
||||
]
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==13.3.2"]
|
||||
"requirements": ["aioamazondevices==13.4.0"]
|
||||
}
|
||||
|
||||
@@ -71,6 +71,43 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a flow initiated by zeroconf."""
|
||||
return self._async_create_entry()
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of an existing Elgato device."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
elgato = Elgato(
|
||||
host=user_input[CONF_HOST],
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
|
||||
try:
|
||||
info = await elgato.info()
|
||||
except ElgatoError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(info.serial_number)
|
||||
self._abort_if_unique_id_mismatch(reason="different_device")
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data_updates={CONF_HOST: user_input[CONF_HOST]},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_HOST,
|
||||
default=self._get_reconfigure_entry().data[CONF_HOST],
|
||||
): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -62,7 +62,7 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
|
||||
@@ -3,13 +3,23 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
||||
"different_device": "The configured Elgato device is not the same as the one at this address.",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"flow_title": "{serial_number}",
|
||||
"step": {
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::elgato::config::step::user::data_description::host%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "epic_games_store",
|
||||
"name": "Epic Games Store",
|
||||
"codeowners": ["@hacf-fr", "@Quentame"],
|
||||
"codeowners": ["@Quentame"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/epic_games_store",
|
||||
"integration_type": "service",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "freebox",
|
||||
"name": "Freebox",
|
||||
"codeowners": ["@hacf-fr", "@Quentame"],
|
||||
"codeowners": ["@hacf-fr/reviewers", "@Quentame"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["ffmpeg"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/freebox",
|
||||
|
||||
@@ -27,6 +27,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from .const import ATTR_SENSOR_ID, CONF_SENSOR_ID, DOMAIN
|
||||
from .coordinator import LuftdatenConfigEntry, LuftdatenDataUpdateCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="temperature",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["mastodon"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["Mastodon.py==2.1.2"]
|
||||
"requirements": ["Mastodon.py==2.2.1"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["melcloud"],
|
||||
"requirements": ["python-melcloud==0.1.2"]
|
||||
"requirements": ["python-melcloud==0.1.3"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "meteo_france",
|
||||
"name": "M\u00e9t\u00e9o-France",
|
||||
"codeowners": ["@hacf-fr", "@oncleben31", "@Quentame"],
|
||||
"codeowners": ["@hacf-fr/reviewers", "@oncleben31", "@Quentame"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/meteo_france",
|
||||
"integration_type": "service",
|
||||
|
||||
@@ -59,6 +59,7 @@ DEFAULT_PLATE_COUNT = 4
|
||||
|
||||
PLATE_COUNT = {
|
||||
"KM7575": 6,
|
||||
"KM7576": 6,
|
||||
"KM7678": 6,
|
||||
"KM7697": 6,
|
||||
"KM7699": 5,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "netgear",
|
||||
"name": "NETGEAR",
|
||||
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
|
||||
"codeowners": ["@Quentame", "@starkillerOG"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/netgear",
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["satel_integra"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["satel-integra==1.1.0"]
|
||||
"requirements": ["satel-integra==1.1.1"]
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["PySrDaliGateway==0.19.3"]
|
||||
"requirements": ["PySrDaliGateway==0.20.4"]
|
||||
}
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["switchbot"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["PySwitchbot==2.0.0"]
|
||||
"requirements": ["PySwitchbot==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "synology_dsm",
|
||||
"name": "Synology DSM",
|
||||
"codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
|
||||
"codeowners": ["@Quentame", "@mib1185"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
|
||||
|
||||
@@ -68,7 +68,6 @@ class TailwindButtonEntity(TailwindEntity, ButtonEntity):
|
||||
await self.entity_description.press_fn(self.coordinator.tailwind)
|
||||
except TailwindError as exc:
|
||||
raise HomeAssistantError(
|
||||
str(exc),
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
) from exc
|
||||
|
||||
@@ -5,6 +5,7 @@ from datetime import timedelta
|
||||
from gotailwind import (
|
||||
Tailwind,
|
||||
TailwindAuthenticationError,
|
||||
TailwindConnectionError,
|
||||
TailwindDeviceStatus,
|
||||
TailwindError,
|
||||
)
|
||||
@@ -45,5 +46,13 @@ class TailwindDataUpdateCoordinator(DataUpdateCoordinator[TailwindDeviceStatus])
|
||||
return await self.tailwind.status()
|
||||
except TailwindAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except TailwindConnectionError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
) from err
|
||||
except TailwindError as err:
|
||||
raise UpdateFailed(err) from err
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unknown_error",
|
||||
) from err
|
||||
|
||||
@@ -55,10 +55,7 @@ rules:
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations:
|
||||
status: exempt
|
||||
comment: |
|
||||
The coordinator needs translation when the update failed.
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
|
||||
@@ -83,6 +83,9 @@
|
||||
},
|
||||
"door_locked_out": {
|
||||
"message": "The door is locked out and cannot be operated."
|
||||
},
|
||||
"unknown_error": {
|
||||
"message": "An unknown error occurred while communicating with the Tailwind device."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
homeassistant/components/unifi_discovery/__init__.py
Normal file
18
homeassistant/components/unifi_discovery/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""The UniFi Discovery integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .discovery import async_start_discovery
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up UniFi Discovery."""
|
||||
async_start_discovery(hass)
|
||||
return True
|
||||
46
homeassistant/components/unifi_discovery/config_flow.py
Normal file
46
homeassistant/components/unifi_discovery/config_flow.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Config flow for UniFi Discovery."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo
|
||||
|
||||
from .const import DOMAIN
|
||||
from .discovery import async_start_discovery
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UnifiDiscoveryFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for UniFi Discovery."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a user-initiated flow."""
|
||||
async_start_discovery(self.hass)
|
||||
return self.async_abort(reason="discovery_started")
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle discovery via DHCP."""
|
||||
_LOGGER.debug("Starting discovery via DHCP: %s", discovery_info)
|
||||
if self._async_in_progress():
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
async_start_discovery(self.hass)
|
||||
return self.async_abort(reason="discovery_started")
|
||||
|
||||
async def async_step_ssdp(
|
||||
self, discovery_info: SsdpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle discovery via SSDP."""
|
||||
_LOGGER.debug("Starting discovery via SSDP: %s", discovery_info)
|
||||
if self._async_in_progress():
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
async_start_discovery(self.hass)
|
||||
return self.async_abort(reason="discovery_started")
|
||||
12
homeassistant/components/unifi_discovery/const.py
Normal file
12
homeassistant/components/unifi_discovery/const.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Constants for the UniFi Discovery integration."""
|
||||
|
||||
from unifi_discovery import UnifiService
|
||||
|
||||
DOMAIN = "unifi_discovery"
|
||||
|
||||
# Static mapping of UniFi service types to their Home Assistant integration domains.
|
||||
# This must be static (not a runtime registry) because consumers may not be loaded
|
||||
# when initial discovery runs — the same pattern DHCP/SSDP use with manifest matchers.
|
||||
CONSUMER_MAPPING: dict[UnifiService, str] = {
|
||||
UnifiService.Protect: "unifiprotect",
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
"""The unifiprotect integration discovery."""
|
||||
"""UniFi network device discovery."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from dataclasses import asdict
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from unifi_discovery import AIOUnifiScanner, UnifiDevice, UnifiService
|
||||
from unifi_discovery import AIOUnifiScanner, UnifiDevice
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -15,32 +15,21 @@ from homeassistant.helpers import discovery_flow
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONSUMER_MAPPING, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UniFiProtectRuntimeData:
|
||||
"""Runtime data stored in hass.data[DOMAIN]."""
|
||||
|
||||
auth_retries: dict[str, int] = field(default_factory=dict)
|
||||
discovery_started: bool = False
|
||||
|
||||
|
||||
# Typed key for hass.data access at DOMAIN level
|
||||
DATA_UNIFIPROTECT: HassKey[UniFiProtectRuntimeData] = HassKey(DOMAIN)
|
||||
|
||||
DISCOVERY_INTERVAL = timedelta(minutes=60)
|
||||
|
||||
DATA_DISCOVERY_STARTED: HassKey[bool] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
@callback
|
||||
def async_start_discovery(hass: HomeAssistant) -> None:
|
||||
"""Start discovery."""
|
||||
domain_data = hass.data.setdefault(DATA_UNIFIPROTECT, UniFiProtectRuntimeData())
|
||||
if domain_data.discovery_started:
|
||||
"""Start discovery of UniFi devices."""
|
||||
if hass.data.get(DATA_DISCOVERY_STARTED):
|
||||
return
|
||||
domain_data.discovery_started = True
|
||||
hass.data[DATA_DISCOVERY_STARTED] = True
|
||||
|
||||
async def _async_discovery() -> None:
|
||||
async_trigger_discovery(hass, await async_discover_devices())
|
||||
@@ -48,7 +37,9 @@ def async_start_discovery(hass: HomeAssistant) -> None:
|
||||
@callback
|
||||
def _async_start_background_discovery(*_: Any) -> None:
|
||||
"""Run discovery in the background."""
|
||||
hass.async_create_background_task(_async_discovery(), "unifiprotect-discovery")
|
||||
hass.async_create_background_task(
|
||||
_async_discovery(), "unifi_discovery-discovery"
|
||||
)
|
||||
|
||||
# Do not block startup since discovery takes 31s or more
|
||||
_async_start_background_discovery()
|
||||
@@ -61,7 +52,7 @@ def async_start_discovery(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
async def async_discover_devices() -> list[UnifiDevice]:
|
||||
"""Discover devices."""
|
||||
"""Discover UniFi devices on the network."""
|
||||
scanner = AIOUnifiScanner()
|
||||
devices = await scanner.async_scan()
|
||||
_LOGGER.debug("Found devices: %s", devices)
|
||||
@@ -75,10 +66,13 @@ def async_trigger_discovery(
|
||||
) -> None:
|
||||
"""Trigger config flows for discovered devices."""
|
||||
for device in discovered_devices:
|
||||
if device.services[UnifiService.Protect] and device.hw_addr:
|
||||
discovery_flow.async_create_flow(
|
||||
hass,
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=asdict(device),
|
||||
)
|
||||
if not device.hw_addr:
|
||||
continue
|
||||
for service, domain in CONSUMER_MAPPING.items():
|
||||
if device.services.get(service):
|
||||
discovery_flow.async_create_flow(
|
||||
hass,
|
||||
domain,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=asdict(device),
|
||||
)
|
||||
63
homeassistant/components/unifi_discovery/manifest.json
Normal file
63
homeassistant/components/unifi_discovery/manifest.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"domain": "unifi_discovery",
|
||||
"name": "UniFi Discovery",
|
||||
"codeowners": ["@RaHehl"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
"macaddress": "B4FBE4*"
|
||||
},
|
||||
{
|
||||
"macaddress": "802AA8*"
|
||||
},
|
||||
{
|
||||
"macaddress": "F09FC2*"
|
||||
},
|
||||
{
|
||||
"macaddress": "68D79A*"
|
||||
},
|
||||
{
|
||||
"macaddress": "18E829*"
|
||||
},
|
||||
{
|
||||
"macaddress": "245A4C*"
|
||||
},
|
||||
{
|
||||
"macaddress": "784558*"
|
||||
},
|
||||
{
|
||||
"macaddress": "E063DA*"
|
||||
},
|
||||
{
|
||||
"macaddress": "265A4C*"
|
||||
},
|
||||
{
|
||||
"macaddress": "74ACB9*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/unifi_discovery",
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["unifi_discovery"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["unifi-discovery==1.4.0"],
|
||||
"single_config_entry": true,
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine Pro"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine SE"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine Pro Max"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
homeassistant/components/unifi_discovery/strings.json
Normal file
13
homeassistant/components/unifi_discovery/strings.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"discovery_started": "Discovery started"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "UniFi Discovery is set up automatically."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ from .const import (
|
||||
PLATFORMS,
|
||||
)
|
||||
from .data import ProtectData, UFPConfigEntry
|
||||
from .discovery import DATA_UNIFIPROTECT, UniFiProtectRuntimeData, async_start_discovery
|
||||
from .migrate import async_migrate_data
|
||||
from .services import async_setup_services
|
||||
from .utils import (
|
||||
@@ -64,11 +63,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the UniFi Protect."""
|
||||
# Initialize domain data structure (setdefault in case discovery already started)
|
||||
hass.data.setdefault(DATA_UNIFIPROTECT, UniFiProtectRuntimeData())
|
||||
# Only start discovery once regardless of how many entries they have
|
||||
async_setup_services(hass)
|
||||
async_start_discovery(hass)
|
||||
return True
|
||||
|
||||
|
||||
@@ -78,20 +73,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool:
|
||||
protect = async_create_api_client(hass, entry)
|
||||
_LOGGER.debug("Connect to UniFi Protect")
|
||||
|
||||
# Reuse ProtectData from previous retry or create new
|
||||
if hasattr(entry, "runtime_data"):
|
||||
data_service = entry.runtime_data
|
||||
data_service.api = protect
|
||||
else:
|
||||
data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry)
|
||||
entry.runtime_data = data_service
|
||||
|
||||
try:
|
||||
await protect.update()
|
||||
except NotAuthorized as err:
|
||||
domain_data = hass.data.setdefault(DATA_UNIFIPROTECT, UniFiProtectRuntimeData())
|
||||
retries = domain_data.auth_retries.get(entry.entry_id, 0)
|
||||
if retries < AUTH_RETRIES:
|
||||
retries += 1
|
||||
domain_data.auth_retries[entry.entry_id] = retries
|
||||
raise ConfigEntryNotReady from err
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
data_service.auth_retries += 1
|
||||
if data_service.auth_retries > AUTH_RETRIES:
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
raise ConfigEntryNotReady from err
|
||||
except (TimeoutError, ClientError, ServerDisconnectedError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry)
|
||||
bootstrap = protect.bootstrap
|
||||
nvr_info = bootstrap.nvr
|
||||
auth_user = bootstrap.users.get(bootstrap.auth_user_id)
|
||||
@@ -142,7 +140,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool:
|
||||
if entry.unique_id is None:
|
||||
hass.config_entries.async_update_entry(entry, unique_id=nvr_info.mac)
|
||||
|
||||
entry.runtime_data = data_service
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop)
|
||||
)
|
||||
|
||||
@@ -36,8 +36,6 @@ from homeassistant.helpers.aiohttp_client import (
|
||||
async_create_clientsession,
|
||||
async_get_clientsession,
|
||||
)
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo
|
||||
from homeassistant.helpers.storage import STORAGE_DIR
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
from homeassistant.loader import async_get_integration
|
||||
@@ -56,7 +54,6 @@ from .const import (
|
||||
OUTDATED_LOG_MESSAGE,
|
||||
)
|
||||
from .data import UFPConfigEntry, async_last_update_was_successful
|
||||
from .discovery import async_start_discovery
|
||||
from .utils import (
|
||||
_async_resolve,
|
||||
_async_short_mac,
|
||||
@@ -205,28 +202,6 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
super().__init__()
|
||||
self._discovered_device: dict[str, str] = {}
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle discovery via dhcp."""
|
||||
_LOGGER.debug("Starting discovery via: %s", discovery_info)
|
||||
return await self._async_discovery_handoff()
|
||||
|
||||
async def async_step_ssdp(
|
||||
self, discovery_info: SsdpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a discovered UniFi device."""
|
||||
_LOGGER.debug("Starting discovery via: %s", discovery_info)
|
||||
return await self._async_discovery_handoff()
|
||||
|
||||
async def _async_discovery_handoff(self) -> ConfigFlowResult:
|
||||
"""Ensure discovery is active."""
|
||||
# Discovery requires an additional check so we use
|
||||
# SSDP and DHCP to tell us to start it so it only
|
||||
# runs on networks where unifi devices are present.
|
||||
async_start_discovery(self.hass)
|
||||
return self.async_abort(reason="discovery_started")
|
||||
|
||||
async def async_step_integration_discovery(
|
||||
self, discovery_info: DiscoveryInfoType
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -86,6 +86,7 @@ class ProtectData:
|
||||
self._pending_camera_ids: set[str] = set()
|
||||
self._unsubs: list[CALLBACK_TYPE] = []
|
||||
self._auth_failures = 0
|
||||
self.auth_retries = 0
|
||||
self.last_update_success = False
|
||||
self.api = protect
|
||||
self.adopt_signal = _async_dispatch_id(entry, DISPATCH_ADOPT)
|
||||
|
||||
@@ -3,61 +3,11 @@
|
||||
"name": "UniFi Protect",
|
||||
"codeowners": ["@RaHehl"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["http", "repairs"],
|
||||
"dhcp": [
|
||||
{
|
||||
"macaddress": "B4FBE4*"
|
||||
},
|
||||
{
|
||||
"macaddress": "802AA8*"
|
||||
},
|
||||
{
|
||||
"macaddress": "F09FC2*"
|
||||
},
|
||||
{
|
||||
"macaddress": "68D79A*"
|
||||
},
|
||||
{
|
||||
"macaddress": "18E829*"
|
||||
},
|
||||
{
|
||||
"macaddress": "245A4C*"
|
||||
},
|
||||
{
|
||||
"macaddress": "784558*"
|
||||
},
|
||||
{
|
||||
"macaddress": "E063DA*"
|
||||
},
|
||||
{
|
||||
"macaddress": "265A4C*"
|
||||
},
|
||||
{
|
||||
"macaddress": "74ACB9*"
|
||||
}
|
||||
],
|
||||
"dependencies": ["http", "repairs", "unifi_discovery"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/unifiprotect",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"loggers": ["uiprotect"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["uiprotect==10.2.6", "unifi-discovery==1.4.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine Pro"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine SE"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine Pro Max"
|
||||
}
|
||||
]
|
||||
"requirements": ["uiprotect==10.2.6"]
|
||||
}
|
||||
|
||||
@@ -39,7 +39,9 @@ rules:
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: Discovery is handled via unifi_discovery dependency using SOURCE_INTEGRATION_DISCOVERY.
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
|
||||
@@ -18,6 +18,7 @@ PLATFORMS = [
|
||||
UNSUPPORTED_DEVICES = [
|
||||
"Heatbox1",
|
||||
"Heatbox2_SRC",
|
||||
"Heatbox3",
|
||||
"E3_TCU10_x07",
|
||||
"E3_TCU41_x04",
|
||||
"E3_RoomControl_One_522",
|
||||
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -766,6 +766,7 @@ FLOWS = {
|
||||
"ukraine_alarm",
|
||||
"unifi",
|
||||
"unifi_access",
|
||||
"unifi_discovery",
|
||||
"unifiprotect",
|
||||
"upb",
|
||||
"upcloud",
|
||||
|
||||
20
homeassistant/generated/dhcp.py
generated
20
homeassistant/generated/dhcp.py
generated
@@ -1331,43 +1331,43 @@ DHCP: Final[list[dict[str, str | bool]]] = [
|
||||
"hostname": "twinkly-*",
|
||||
},
|
||||
{
|
||||
"domain": "unifiprotect",
|
||||
"domain": "unifi_discovery",
|
||||
"macaddress": "B4FBE4*",
|
||||
},
|
||||
{
|
||||
"domain": "unifiprotect",
|
||||
"domain": "unifi_discovery",
|
||||
"macaddress": "802AA8*",
|
||||
},
|
||||
{
|
||||
"domain": "unifiprotect",
|
||||
"domain": "unifi_discovery",
|
||||
"macaddress": "F09FC2*",
|
||||
},
|
||||
{
|
||||
"domain": "unifiprotect",
|
||||
"domain": "unifi_discovery",
|
||||
"macaddress": "68D79A*",
|
||||
},
|
||||
{
|
||||
"domain": "unifiprotect",
|
||||
"domain": "unifi_discovery",
|
||||
"macaddress": "18E829*",
|
||||
},
|
||||
{
|
||||
"domain": "unifiprotect",
|
||||
"domain": "unifi_discovery",
|
||||
"macaddress": "245A4C*",
|
||||
},
|
||||
{
|
||||
"domain": "unifiprotect",
|
||||
"domain": "unifi_discovery",
|
||||
"macaddress": "784558*",
|
||||
},
|
||||
{
|
||||
"domain": "unifiprotect",
|
||||
"domain": "unifi_discovery",
|
||||
"macaddress": "E063DA*",
|
||||
},
|
||||
{
|
||||
"domain": "unifiprotect",
|
||||
"domain": "unifi_discovery",
|
||||
"macaddress": "265A4C*",
|
||||
},
|
||||
{
|
||||
"domain": "unifiprotect",
|
||||
"domain": "unifi_discovery",
|
||||
"macaddress": "74ACB9*",
|
||||
},
|
||||
{
|
||||
|
||||
2
homeassistant/generated/ssdp.py
generated
2
homeassistant/generated/ssdp.py
generated
@@ -359,7 +359,7 @@ SSDP = {
|
||||
"modelDescription": "UniFi Dream Machine Pro Max",
|
||||
},
|
||||
],
|
||||
"unifiprotect": [
|
||||
"unifi_discovery": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine",
|
||||
|
||||
@@ -1434,16 +1434,16 @@ class IntentResponse:
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return a dictionary representation of an intent response."""
|
||||
response_dict: dict[str, Any] = {
|
||||
"speech": self.speech,
|
||||
"card": self.card,
|
||||
"speech": {k: dict(v) for k, v in self.speech.items()},
|
||||
"card": {k: dict(v) for k, v in self.card.items()},
|
||||
"language": self.language,
|
||||
"response_type": self.response_type.value,
|
||||
}
|
||||
|
||||
if self.reprompt:
|
||||
response_dict["reprompt"] = self.reprompt
|
||||
response_dict["reprompt"] = {k: dict(v) for k, v in self.reprompt.items()}
|
||||
if self.speech_slots:
|
||||
response_dict["speech_slots"] = self.speech_slots
|
||||
response_dict["speech_slots"] = self.speech_slots.copy()
|
||||
|
||||
response_data: dict[str, Any] = {}
|
||||
|
||||
|
||||
14
requirements_all.txt
generated
14
requirements_all.txt
generated
@@ -25,7 +25,7 @@ HATasmota==0.10.1
|
||||
HueBLE==2.1.0
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==2.1.2
|
||||
Mastodon.py==2.2.1
|
||||
|
||||
# homeassistant.components.playstation_network
|
||||
PSNAWP==3.0.3
|
||||
@@ -80,10 +80,10 @@ PyQRCode==1.2.1
|
||||
PyRMVtransport==0.3.3
|
||||
|
||||
# homeassistant.components.sunricher_dali
|
||||
PySrDaliGateway==0.19.3
|
||||
PySrDaliGateway==0.20.4
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==2.0.0
|
||||
PySwitchbot==2.0.1
|
||||
|
||||
# homeassistant.components.switchmate
|
||||
PySwitchmate==0.5.1
|
||||
@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==13.3.2
|
||||
aioamazondevices==13.4.0
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -2617,7 +2617,7 @@ python-kasa[speedups]==0.10.2
|
||||
python-linkplay==0.2.12
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
python-melcloud==0.1.2
|
||||
python-melcloud==0.1.3
|
||||
|
||||
# homeassistant.components.xiaomi_miio
|
||||
python-miio==0.5.12
|
||||
@@ -2886,7 +2886,7 @@ samsungtvws[async,encrypted]==2.7.2
|
||||
sanix==1.0.6
|
||||
|
||||
# homeassistant.components.satel_integra
|
||||
satel-integra==1.1.0
|
||||
satel-integra==1.1.1
|
||||
|
||||
# homeassistant.components.screenlogic
|
||||
screenlogicpy==0.10.2
|
||||
@@ -3186,7 +3186,7 @@ uiprotect==10.2.6
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
# homeassistant.components.unifi_discovery
|
||||
unifi-discovery==1.4.0
|
||||
|
||||
# homeassistant.components.unifi_direct
|
||||
|
||||
14
requirements_test_all.txt
generated
14
requirements_test_all.txt
generated
@@ -25,7 +25,7 @@ HATasmota==0.10.1
|
||||
HueBLE==2.1.0
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==2.1.2
|
||||
Mastodon.py==2.2.1
|
||||
|
||||
# homeassistant.components.playstation_network
|
||||
PSNAWP==3.0.3
|
||||
@@ -80,10 +80,10 @@ PyQRCode==1.2.1
|
||||
PyRMVtransport==0.3.3
|
||||
|
||||
# homeassistant.components.sunricher_dali
|
||||
PySrDaliGateway==0.19.3
|
||||
PySrDaliGateway==0.20.4
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==2.0.0
|
||||
PySwitchbot==2.0.1
|
||||
|
||||
# homeassistant.components.syncthru
|
||||
PySyncThru==0.8.0
|
||||
@@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==13.3.2
|
||||
aioamazondevices==13.4.0
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -2219,7 +2219,7 @@ python-kasa[speedups]==0.10.2
|
||||
python-linkplay==0.2.12
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
python-melcloud==0.1.2
|
||||
python-melcloud==0.1.3
|
||||
|
||||
# homeassistant.components.xiaomi_miio
|
||||
python-miio==0.5.12
|
||||
@@ -2449,7 +2449,7 @@ samsungtvws[async,encrypted]==2.7.2
|
||||
sanix==1.0.6
|
||||
|
||||
# homeassistant.components.satel_integra
|
||||
satel-integra==1.1.0
|
||||
satel-integra==1.1.1
|
||||
|
||||
# homeassistant.components.screenlogic
|
||||
screenlogicpy==0.10.2
|
||||
@@ -2698,7 +2698,7 @@ uiprotect==10.2.6
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
# homeassistant.components.unifi_discovery
|
||||
unifi-discovery==1.4.0
|
||||
|
||||
# homeassistant.components.homeassistant_hardware
|
||||
|
||||
@@ -9,7 +9,7 @@ from .brand import validate as validate_brands
|
||||
from .model import Brand, Config, Integration, IntegrationType
|
||||
from .serializer import format_python_namespace
|
||||
|
||||
UNIQUE_ID_IGNORE = {"huawei_lte", "mqtt", "adguard"}
|
||||
UNIQUE_ID_IGNORE = {"huawei_lte", "mqtt", "adguard", "unifi_discovery"}
|
||||
|
||||
|
||||
def _validate_integration(config: Config, integration: Integration) -> None:
|
||||
|
||||
@@ -2151,6 +2151,7 @@ NO_QUALITY_SCALE = [
|
||||
"search",
|
||||
"system_health",
|
||||
"system_log",
|
||||
"unifi_discovery",
|
||||
"tag",
|
||||
"temperature",
|
||||
"timer",
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aioamazondevices.api import AmazonDeviceSensor
|
||||
from aioamazondevices.exceptions import (
|
||||
CannotAuthenticate,
|
||||
CannotConnect,
|
||||
CannotRetrieveData,
|
||||
)
|
||||
from aioamazondevices.structures import AmazonDeviceSensor
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from copy import deepcopy
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aioamazondevices.api import AmazonDeviceSensor
|
||||
from aioamazondevices.structures import AmazonDeviceSensor
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -11,6 +11,7 @@ async def test_async_get_result_from_chat_log(
|
||||
) -> None:
|
||||
"""Test getting result from chat log."""
|
||||
intent_response = intent.IntentResponse(language="en")
|
||||
tool_result = llm.IntentResponseDict(intent_response)
|
||||
with (
|
||||
chat_session.async_get_chat_session(hass) as session,
|
||||
conversation.async_get_chat_log(
|
||||
@@ -23,7 +24,7 @@ async def test_async_get_result_from_chat_log(
|
||||
agent_id="mock-agent-id",
|
||||
tool_call_id="mock-tool-call-id",
|
||||
tool_name="mock-tool-name",
|
||||
tool_result=llm.IntentResponseDict(intent_response),
|
||||
tool_result=tool_result,
|
||||
),
|
||||
conversation.AssistantContent(
|
||||
agent_id="mock-agent-id",
|
||||
@@ -37,3 +38,4 @@ async def test_async_get_result_from_chat_log(
|
||||
# Original intent response is returned with speech set
|
||||
assert result.response is intent_response
|
||||
assert result.response.speech["plain"]["speech"] == "This is a response."
|
||||
assert tool_result["speech"] != result.response.speech
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from elgato import ElgatoConnectionError
|
||||
from elgato import ElgatoConnectionError, ElgatoError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.elgato.const import DOMAIN
|
||||
@@ -331,3 +331,76 @@ async def test_dhcp_discovery_no_match(
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.1"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_elgato")
|
||||
async def test_reconfigure_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reconfiguring an existing Elgato device."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "127.0.0.42"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.42"
|
||||
|
||||
|
||||
async def test_reconfigure_flow_cannot_connect(
|
||||
hass: HomeAssistant,
|
||||
mock_elgato: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reconfigure flow recovers from a connection error."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
mock_elgato.info.side_effect = ElgatoError
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "127.0.0.42"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
mock_elgato.info.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "127.0.0.42"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
|
||||
|
||||
async def test_reconfigure_flow_different_device(
|
||||
hass: HomeAssistant,
|
||||
mock_elgato: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reconfigure aborts when the device at the new host has a different serial."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
mock_elgato.info.return_value.serial_number = "DIFFERENT_SERIAL"
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "127.0.0.42"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "different_device"
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.1"
|
||||
|
||||
@@ -1 +1,67 @@
|
||||
"""Tests for the Eurotronic Comet Blue integration."""
|
||||
|
||||
from eurotronic_cometblue_ha import const as cometblue_const
|
||||
|
||||
from homeassistant.const import CONF_PIN
|
||||
|
||||
FIXTURE_DEVICE_NAME = "Comet Blue"
|
||||
FIXTURE_MAC = "aa:bb:cc:dd:ee:ff"
|
||||
FIXTURE_RSSI = -60
|
||||
FIXTURE_SERVICE_UUID = "47e9ee00-47e9-11e4-8939-164230d1df67"
|
||||
|
||||
WRITEABLE_CHARACTERISTICS = [
|
||||
cometblue_const.CHARACTERISTIC_DATETIME,
|
||||
cometblue_const.CHARACTERISTIC_MONDAY,
|
||||
cometblue_const.CHARACTERISTIC_TUESDAY,
|
||||
cometblue_const.CHARACTERISTIC_WEDNESDAY,
|
||||
cometblue_const.CHARACTERISTIC_THURSDAY,
|
||||
cometblue_const.CHARACTERISTIC_FRIDAY,
|
||||
cometblue_const.CHARACTERISTIC_SATURDAY,
|
||||
cometblue_const.CHARACTERISTIC_SUNDAY,
|
||||
cometblue_const.CHARACTERISTIC_HOLIDAY_1,
|
||||
cometblue_const.CHARACTERISTIC_SETTINGS,
|
||||
cometblue_const.CHARACTERISTIC_TEMPERATURE,
|
||||
cometblue_const.CHARACTERISTIC_PIN,
|
||||
]
|
||||
WRITEABLE_CHARACTERISTICS_ALLOW_UNCHANGED = [
|
||||
cometblue_const.CHARACTERISTIC_SETTINGS,
|
||||
cometblue_const.CHARACTERISTIC_TEMPERATURE,
|
||||
]
|
||||
|
||||
FIXTURE_DEFAULT_CHARACTERISTICS = {
|
||||
cometblue_const.CHARACTERISTIC_MODEL: b"Comet Blue",
|
||||
cometblue_const.CHARACTERISTIC_VERSION: b"0.0.10",
|
||||
cometblue_const.CHARACTERISTIC_MANUFACTURER: b"Eurotronic GmbH",
|
||||
cometblue_const.CHARACTERISTIC_HOLIDAY_1: [
|
||||
128,
|
||||
27,
|
||||
11,
|
||||
22,
|
||||
128,
|
||||
27,
|
||||
11,
|
||||
22,
|
||||
34,
|
||||
],
|
||||
cometblue_const.CHARACTERISTIC_TEMPERATURE: [
|
||||
41,
|
||||
40,
|
||||
34,
|
||||
42,
|
||||
0,
|
||||
4,
|
||||
10,
|
||||
],
|
||||
cometblue_const.CHARACTERISTIC_BATTERY: b"48",
|
||||
cometblue_const.CHARACTERISTIC_MONDAY: [37, 137, 0, 0, 0, 0, 0, 0],
|
||||
cometblue_const.CHARACTERISTIC_TUESDAY: [37, 137, 0, 0, 0, 0, 0, 0],
|
||||
cometblue_const.CHARACTERISTIC_WEDNESDAY: [37, 137, 0, 0, 0, 0, 0, 0],
|
||||
cometblue_const.CHARACTERISTIC_THURSDAY: [37, 137, 0, 0, 0, 0, 0, 0],
|
||||
cometblue_const.CHARACTERISTIC_FRIDAY: [0, 1, 10, 20, 21, 130, 140, 143],
|
||||
cometblue_const.CHARACTERISTIC_SATURDAY: [37, 137, 0, 0, 0, 0, 0, 0],
|
||||
cometblue_const.CHARACTERISTIC_SUNDAY: [37, 137, 0, 0, 0, 0, 0, 0],
|
||||
}
|
||||
|
||||
FIXTURE_USER_INPUT = {
|
||||
CONF_PIN: "000000",
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Session fixtures."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Buffer, Generator
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
import uuid
|
||||
@@ -12,17 +12,21 @@ from eurotronic_cometblue_ha import CometBlueBleakClient
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
|
||||
from homeassistant.components.eurotronic_cometblue import PLATFORMS
|
||||
from homeassistant.components.eurotronic_cometblue.const import DOMAIN
|
||||
from homeassistant.const import CONF_ADDRESS
|
||||
from homeassistant.const import CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import (
|
||||
from . import (
|
||||
FIXTURE_DEFAULT_CHARACTERISTICS,
|
||||
FIXTURE_DEVICE_NAME,
|
||||
FIXTURE_GATT_CHARACTERISTICS,
|
||||
FIXTURE_MAC,
|
||||
FIXTURE_RSSI,
|
||||
FIXTURE_SERVICE_UUID,
|
||||
FIXTURE_USER_INPUT,
|
||||
WRITEABLE_CHARACTERISTICS,
|
||||
WRITEABLE_CHARACTERISTICS_ALLOW_UNCHANGED,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -58,9 +62,24 @@ FAKE_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||
)
|
||||
|
||||
|
||||
def _normalize_characteristic(
|
||||
char_specifier: BleakGATTCharacteristic | int | str | uuid.UUID,
|
||||
) -> uuid.UUID:
|
||||
"""Normalize a characteristic specifier to UUID."""
|
||||
if not isinstance(char_specifier, (BleakGATTCharacteristic, str, uuid.UUID)):
|
||||
raise BleakCharacteristicNotFoundError(char_specifier)
|
||||
if isinstance(char_specifier, BleakGATTCharacteristic):
|
||||
char_specifier = char_specifier.uuid
|
||||
if not isinstance(char_specifier, uuid.UUID):
|
||||
char_specifier = uuid.UUID(char_specifier)
|
||||
return char_specifier
|
||||
|
||||
|
||||
class MockCometBlueBleakClient(CometBlueBleakClient):
|
||||
"""Mock BleakClient."""
|
||||
|
||||
characteristics: dict[uuid.UUID, bytearray] = {}
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Mock init."""
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -94,17 +113,40 @@ class MockCometBlueBleakClient(CometBlueBleakClient):
|
||||
**kwargs: Any,
|
||||
) -> bytearray:
|
||||
"""Mock read_gatt_char."""
|
||||
if not isinstance(char_specifier, (BleakGATTCharacteristic, str, uuid.UUID)):
|
||||
raise BleakCharacteristicNotFoundError(char_specifier)
|
||||
if isinstance(char_specifier, BleakGATTCharacteristic):
|
||||
char_specifier = char_specifier.uuid
|
||||
if not isinstance(char_specifier, uuid.UUID):
|
||||
char_specifier = uuid.UUID(char_specifier)
|
||||
char_specifier = _normalize_characteristic(char_specifier)
|
||||
try:
|
||||
return FIXTURE_GATT_CHARACTERISTICS[char_specifier]
|
||||
return bytearray(self.characteristics[char_specifier])
|
||||
except KeyError:
|
||||
raise BleakCharacteristicNotFoundError(char_specifier)
|
||||
|
||||
async def write_gatt_char(
|
||||
self,
|
||||
char_specifier: BleakGATTCharacteristic | int | str | uuid.UUID,
|
||||
data: Buffer,
|
||||
response: bool | None = None,
|
||||
) -> None:
|
||||
"""Mock write_gatt_char."""
|
||||
char_specifier = _normalize_characteristic(char_specifier)
|
||||
if char_specifier not in WRITEABLE_CHARACTERISTICS:
|
||||
raise BleakCharacteristicNotFoundError(char_specifier)
|
||||
data = bytearray(data)
|
||||
# when writing temperature it is possible that 128 will be sent, meaning "no change"
|
||||
# we have to restore the original value in this case to keep tests working
|
||||
if char_specifier in WRITEABLE_CHARACTERISTICS_ALLOW_UNCHANGED:
|
||||
for i, byte in enumerate(data):
|
||||
if byte == 128:
|
||||
data[i] = self.characteristics[char_specifier][i]
|
||||
self.characteristics[char_specifier] = data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_gatt_characteristics() -> dict[uuid.UUID, bytearray]:
|
||||
"""Provide a mutable per-test GATT characteristic store."""
|
||||
return {
|
||||
characteristic: bytearray(value)
|
||||
for characteristic, value in FIXTURE_DEFAULT_CHARACTERISTICS.items()
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_service_info() -> Generator[None]:
|
||||
@@ -133,13 +175,26 @@ def mock_ble_device() -> Generator[None]:
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_bluetooth(enable_bluetooth: None) -> Generator[None]:
|
||||
def mock_bluetooth(
|
||||
enable_bluetooth: None,
|
||||
mock_gatt_characteristics: dict[uuid.UUID, bytearray],
|
||||
) -> Generator[None]:
|
||||
"""Auto mock bluetooth."""
|
||||
|
||||
with patch(
|
||||
"eurotronic_cometblue_ha.CometBlueBleakClient", MockCometBlueBleakClient
|
||||
MockCometBlueBleakClient.characteristics = mock_gatt_characteristics
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.eurotronic_cometblue.entity.bluetooth.async_address_present",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.eurotronic_cometblue.coordinator.COMMAND_RETRY_INTERVAL",
|
||||
0,
|
||||
),
|
||||
patch("eurotronic_cometblue_ha.CometBlueBleakClient", MockCometBlueBleakClient),
|
||||
):
|
||||
yield
|
||||
MockCometBlueBleakClient.characteristics = {}
|
||||
|
||||
|
||||
# Home Assistant related fixtures
|
||||
@@ -164,3 +219,16 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
return_value=True,
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
||||
|
||||
|
||||
async def setup_with_selected_platforms(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, platforms: list[Platform] | None = None
|
||||
) -> None:
|
||||
"""Set up the Eurotronic Comet Blue integration with the selected platforms."""
|
||||
entry.add_to_hass(hass)
|
||||
with patch(
|
||||
"homeassistant.components.eurotronic_cometblue.PLATFORMS",
|
||||
platforms or PLATFORMS,
|
||||
):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"""Constants for Eurotronic CometBlue tests."""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from homeassistant.const import CONF_PIN
|
||||
|
||||
FIXTURE_DEVICE_NAME = "Comet Blue"
|
||||
FIXTURE_MAC = "aa:bb:cc:dd:ee:ff"
|
||||
FIXTURE_RSSI = -60
|
||||
FIXTURE_SERVICE_UUID = "47e9ee00-47e9-11e4-8939-164230d1df67"
|
||||
|
||||
FIXTURE_GATT_CHARACTERISTICS = {
|
||||
UUID("00002a24-0000-1000-8000-00805f9b34fb"): bytearray(b"Comet Blue"), # model
|
||||
UUID("00002a26-0000-1000-8000-00805f9b34fb"): bytearray(b"0.0.10"), # version
|
||||
UUID("00002a29-0000-1000-8000-00805f9b34fb"): bytearray(
|
||||
b"Eurotronic GmbH"
|
||||
), # manufacturer
|
||||
UUID("47e9ee20-47e9-11e4-8939-164230d1df67"): bytearray(
|
||||
b'\x80\x1b\x0b\x16\x80\x1b\x0b\x16"'
|
||||
), # holiday 1
|
||||
UUID("47e9ee2b-47e9-11e4-8939-164230d1df67"): bytearray(
|
||||
b"/999\x00\x04\n"
|
||||
), # temperature
|
||||
UUID("47e9ee2c-47e9-11e4-8939-164230d1df67"): bytearray(b"48"), # battery
|
||||
}
|
||||
|
||||
FIXTURE_USER_INPUT = {
|
||||
CONF_PIN: "000000",
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
# serializer version: 1
|
||||
# name: test_climate_state[climate.comet_blue_aa_bb_cc_dd_ee_ff-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
<HVACMode.OFF: 'off'>,
|
||||
]),
|
||||
'max_temp': 28.5,
|
||||
'min_temp': 7.5,
|
||||
'preset_modes': list([
|
||||
'comfort',
|
||||
'eco',
|
||||
'boost',
|
||||
'away',
|
||||
'none',
|
||||
]),
|
||||
'target_temp_step': 0.5,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.comet_blue_aa_bb_cc_dd_ee_ff',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'eurotronic_cometblue',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 403>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_climate_state[climate.comet_blue_aa_bb_cc_dd_ee_ff-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 20.5,
|
||||
'friendly_name': 'Comet Blue aa:bb:cc:dd:ee:ff',
|
||||
'hvac_modes': list([
|
||||
<HVACMode.AUTO: 'auto'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
<HVACMode.OFF: 'off'>,
|
||||
]),
|
||||
'max_temp': 28.5,
|
||||
'min_temp': 7.5,
|
||||
'preset_mode': 'none',
|
||||
'preset_modes': list([
|
||||
'comfort',
|
||||
'eco',
|
||||
'boost',
|
||||
'away',
|
||||
'none',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 403>,
|
||||
'target_temp_high': 21.0,
|
||||
'target_temp_low': 17.0,
|
||||
'target_temp_step': 0.5,
|
||||
'temperature': 20.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.comet_blue_aa_bb_cc_dd_ee_ff',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'auto',
|
||||
})
|
||||
# ---
|
||||
331
tests/components/eurotronic_cometblue/test_climate.py
Normal file
331
tests/components/eurotronic_cometblue/test_climate.py
Normal file
@@ -0,0 +1,331 @@
|
||||
"""Test the eurotronic_cometblue climate platform."""
|
||||
|
||||
from unittest.mock import patch
|
||||
import uuid
|
||||
|
||||
from eurotronic_cometblue_ha import const as cometblue_const
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_PRESET_MODE,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
PRESET_AWAY,
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
PRESET_NONE,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.eurotronic_cometblue.climate import MAX_TEMP, MIN_TEMP
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import setup_with_selected_platforms
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
ENTITY_ID = "climate.comet_blue_aa_bb_cc_dd_ee_ff"
|
||||
|
||||
|
||||
async def test_climate_state(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test climate entity state and registry data."""
|
||||
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("temperature_values", "expected_hvac_mode", "expected_preset"),
|
||||
[
|
||||
([47, 15, 34, 42, 0, 4, 10], HVACMode.OFF, PRESET_NONE),
|
||||
([47, 40, 34, 42, 0, 4, 10], HVACMode.AUTO, PRESET_NONE),
|
||||
([47, 42, 34, 42, 0, 4, 10], HVACMode.AUTO, PRESET_COMFORT),
|
||||
([47, 34, 34, 42, 0, 4, 10], HVACMode.AUTO, PRESET_ECO),
|
||||
([47, 57, 57, 57, 0, 4, 10], HVACMode.HEAT, PRESET_BOOST),
|
||||
],
|
||||
)
|
||||
async def test_climate_hvac_and_preset_states(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_gatt_characteristics: dict[uuid.UUID, bytearray],
|
||||
temperature_values: list[int],
|
||||
expected_hvac_mode: HVACMode,
|
||||
expected_preset: str,
|
||||
) -> None:
|
||||
"""Test climate state mapping from device temperatures."""
|
||||
mock_gatt_characteristics[cometblue_const.CHARACTERISTIC_TEMPERATURE] = bytearray(
|
||||
temperature_values
|
||||
)
|
||||
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.state == expected_hvac_mode
|
||||
assert state.attributes[ATTR_PRESET_MODE] == expected_preset
|
||||
|
||||
|
||||
async def test_set_temperature(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test setting target temperature."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 20.0
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 20.5
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 21.0},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 21.0
|
||||
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 20.5
|
||||
|
||||
|
||||
async def test_climate_preset_away_active(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_gatt_characteristics: dict[uuid.UUID, bytearray],
|
||||
) -> None:
|
||||
"""Test away preset detection from holiday data."""
|
||||
# Holiday active if start hour >= 128 and end hour < 128
|
||||
mock_gatt_characteristics[cometblue_const.CHARACTERISTIC_HOLIDAY_1] = bytearray(
|
||||
[128, 1, 1, 26, 10, 2, 1, 26, 34]
|
||||
)
|
||||
# Current target temperature must match holiday temperature for away preset to be active
|
||||
mock_gatt_characteristics[cometblue_const.CHARACTERISTIC_TEMPERATURE] = bytearray(
|
||||
[47, 34, 34, 42, 0, 4, 10]
|
||||
)
|
||||
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY
|
||||
|
||||
with pytest.raises(
|
||||
ServiceValidationError,
|
||||
match="Cannot adjust TRV remotely",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 21.0},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("preset_mode", "expected_temperature", "expected_state"),
|
||||
[
|
||||
(PRESET_ECO, 17.0, HVACMode.AUTO),
|
||||
(PRESET_COMFORT, 21.0, HVACMode.AUTO),
|
||||
(PRESET_BOOST, MAX_TEMP, HVACMode.HEAT),
|
||||
],
|
||||
)
|
||||
async def test_set_preset_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
preset_mode: str,
|
||||
expected_temperature: float,
|
||||
expected_state: HVACMode,
|
||||
) -> None:
|
||||
"""Test setting preset modes."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: preset_mode},
|
||||
blocking=True,
|
||||
)
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.attributes[ATTR_TEMPERATURE] == expected_temperature
|
||||
assert state.attributes[ATTR_PRESET_MODE] == preset_mode
|
||||
assert state.state == expected_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize("preset_mode", [PRESET_NONE, PRESET_AWAY])
|
||||
async def test_set_preset_mode_display_only_raises(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
preset_mode: str,
|
||||
) -> None:
|
||||
"""Test display-only presets cannot be set."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
with pytest.raises(ServiceValidationError, match="Unable to set preset"):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: preset_mode},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("hvac_mode", "expected_temperature", "expected_preset"),
|
||||
[
|
||||
(HVACMode.OFF, MIN_TEMP, PRESET_NONE),
|
||||
(HVACMode.HEAT, MAX_TEMP, PRESET_BOOST),
|
||||
(HVACMode.AUTO, 17.0, PRESET_ECO),
|
||||
],
|
||||
)
|
||||
async def test_set_hvac_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
hvac_mode: HVACMode,
|
||||
expected_temperature: float,
|
||||
expected_preset: str,
|
||||
) -> None:
|
||||
"""Test setting HVAC modes."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: hvac_mode},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.attributes[ATTR_TEMPERATURE] == expected_temperature
|
||||
assert state.attributes[ATTR_PRESET_MODE] == expected_preset
|
||||
assert state.state == hvac_mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "expected_temperature"),
|
||||
[
|
||||
(SERVICE_TURN_OFF, MIN_TEMP),
|
||||
(SERVICE_TURN_ON, 17.0),
|
||||
],
|
||||
)
|
||||
async def test_turn_on_turn_off(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
service: str,
|
||||
expected_temperature: float,
|
||||
) -> None:
|
||||
"""Test turn_on and turn_off services."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 20.0
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.attributes[ATTR_TEMPERATURE] == expected_temperature
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raise_exception", "raised_exception"),
|
||||
[
|
||||
(TimeoutError, HomeAssistantError),
|
||||
(ValueError, ServiceValidationError),
|
||||
],
|
||||
)
|
||||
async def test_set_temperature_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
raise_exception: type[Exception],
|
||||
raised_exception: type[Exception],
|
||||
) -> None:
|
||||
"""Test setting target temperature."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
# raise exceptions to test error handling
|
||||
with (
|
||||
pytest.raises(raised_exception),
|
||||
patch(
|
||||
"homeassistant.components.eurotronic_cometblue.coordinator.AsyncCometBlue.set_temperature_async",
|
||||
side_effect=raise_exception(),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 21.0},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_update_data_error_handling(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that update data errors are handled and retried."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 20.0
|
||||
|
||||
# Fail with TimeoutError (expected) and raise UpdateFailed after 3 retries
|
||||
with patch.object(
|
||||
mock_config_entry.runtime_data.device,
|
||||
"get_temperature_async",
|
||||
side_effect=TimeoutError(),
|
||||
) as mock_get_temperature:
|
||||
await mock_config_entry.runtime_data.async_refresh()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_get_temperature.call_count == 3
|
||||
assert mock_config_entry.runtime_data.last_update_success is False
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 20.0
|
||||
|
||||
# Fail with OSError (unexpected) and raise UpdateFailed directly
|
||||
with patch.object(
|
||||
mock_config_entry.runtime_data.device,
|
||||
"get_temperature_async",
|
||||
side_effect=OSError(),
|
||||
) as mock_get_temperature:
|
||||
await mock_config_entry.runtime_data.async_refresh()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_get_temperature.call_count == 1
|
||||
assert mock_config_entry.runtime_data.last_update_success is False
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 20.0
|
||||
|
||||
# Fail once with TimeoutError and then succeed, verify that data is updated
|
||||
updated_temperatures = dict(mock_config_entry.runtime_data.data.temperatures)
|
||||
updated_temperatures["manualTemp"] = 27.0
|
||||
|
||||
with patch.object(
|
||||
mock_config_entry.runtime_data.device,
|
||||
"get_temperature_async",
|
||||
side_effect=[TimeoutError(), updated_temperatures],
|
||||
) as mock_get_temperature:
|
||||
await mock_config_entry.runtime_data.async_refresh()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_get_temperature.call_count == 2
|
||||
assert mock_config_entry.runtime_data.last_update_success is True
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 27.0
|
||||
@@ -16,8 +16,8 @@ from homeassistant.const import CONF_ADDRESS, CONF_PIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import FIXTURE_DEVICE_NAME, FIXTURE_MAC, FIXTURE_USER_INPUT
|
||||
from .conftest import FAKE_SERVICE_INFO
|
||||
from .const import FIXTURE_DEVICE_NAME, FIXTURE_MAC, FIXTURE_USER_INPUT
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
'account': dict({
|
||||
'acct': 'trwnh',
|
||||
'avatar': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/051c958388818705.png',
|
||||
'avatar_description': None,
|
||||
'avatar_static': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/051c958388818705.png',
|
||||
'bot': True,
|
||||
'created_at': '2016-11-24T00:00:00+00:00',
|
||||
@@ -37,6 +38,7 @@
|
||||
'following_count': 328,
|
||||
'group': False,
|
||||
'header': 'https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg',
|
||||
'header_description': None,
|
||||
'header_static': 'https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg',
|
||||
'hide_collections': True,
|
||||
'id': '14715',
|
||||
@@ -53,6 +55,9 @@
|
||||
'role': None,
|
||||
'roles': list([
|
||||
]),
|
||||
'show_featured': None,
|
||||
'show_media': None,
|
||||
'show_media_replies': None,
|
||||
'source': None,
|
||||
'statuses_count': 69523,
|
||||
'suspended': None,
|
||||
@@ -80,6 +85,7 @@
|
||||
}),
|
||||
}),
|
||||
'version': '4.4.0-nightly.2025-02-07',
|
||||
'wrapstodon': None,
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
@@ -88,6 +94,7 @@
|
||||
'account': dict({
|
||||
'acct': 'trwnh',
|
||||
'avatar': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/051c958388818705.png',
|
||||
'avatar_description': None,
|
||||
'avatar_static': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/051c958388818705.png',
|
||||
'bot': True,
|
||||
'created_at': '2016-11-24T00:00:00+00:00',
|
||||
@@ -121,6 +128,7 @@
|
||||
'following_count': 328,
|
||||
'group': False,
|
||||
'header': 'https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg',
|
||||
'header_description': None,
|
||||
'header_static': 'https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg',
|
||||
'hide_collections': True,
|
||||
'id': '14715',
|
||||
@@ -137,6 +145,9 @@
|
||||
'role': None,
|
||||
'roles': list([
|
||||
]),
|
||||
'show_featured': None,
|
||||
'show_media': None,
|
||||
'show_media_replies': None,
|
||||
'source': None,
|
||||
'statuses_count': 69523,
|
||||
'suspended': None,
|
||||
@@ -164,6 +175,7 @@
|
||||
}),
|
||||
}),
|
||||
'version': '4.4.0-nightly.2025-02-07',
|
||||
'wrapstodon': None,
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -58,6 +58,11 @@
|
||||
'indexable': False,
|
||||
'hide_collections': True,
|
||||
'memorial': None,
|
||||
'avatar_description': None,
|
||||
'header_description': None,
|
||||
'show_media': None,
|
||||
'show_media_replies': None,
|
||||
'show_featured': None,
|
||||
'moved_to_account': None,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -54,7 +54,10 @@ async def test_number_entities(
|
||||
# Test error handling
|
||||
mock_tailwind.identify.side_effect = TailwindError("Some error")
|
||||
|
||||
with pytest.raises(HomeAssistantError, match="Some error") as excinfo:
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="An error occurred while communicating with the Tailwind device",
|
||||
) as excinfo:
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from gotailwind import TailwindAuthenticationError, TailwindConnectionError
|
||||
from gotailwind import (
|
||||
TailwindAuthenticationError,
|
||||
TailwindConnectionError,
|
||||
TailwindError,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.tailwind.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
@@ -31,13 +36,22 @@ async def test_load_unload_config_entry(
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "expected_translation_key"),
|
||||
[
|
||||
(TailwindConnectionError, "communication_error"),
|
||||
(TailwindError, "unknown_error"),
|
||||
],
|
||||
)
|
||||
async def test_config_entry_not_ready(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_tailwind: MagicMock,
|
||||
side_effect: type[Exception],
|
||||
expected_translation_key: str,
|
||||
) -> None:
|
||||
"""Test the Tailwind configuration entry not ready."""
|
||||
mock_tailwind.status.side_effect = TailwindConnectionError
|
||||
mock_tailwind.status.side_effect = side_effect
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
50
tests/components/unifi_discovery/__init__.py
Normal file
50
tests/components/unifi_discovery/__init__.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Tests for the UniFi Discovery integration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from unifi_discovery import AIOUnifiScanner, UnifiDevice, UnifiService
|
||||
|
||||
DEVICE_HOSTNAME = "unvr"
|
||||
DEVICE_IP_ADDRESS = "127.0.0.1"
|
||||
DEVICE_MAC_ADDRESS = "aa:bb:cc:dd:ee:ff"
|
||||
DIRECT_CONNECT_DOMAIN = "x.ui.direct"
|
||||
|
||||
|
||||
UNIFI_DISCOVERY_PROTECT = UnifiDevice(
|
||||
source_ip=DEVICE_IP_ADDRESS,
|
||||
hw_addr=DEVICE_MAC_ADDRESS,
|
||||
platform=DEVICE_HOSTNAME,
|
||||
hostname=DEVICE_HOSTNAME,
|
||||
services={UnifiService.Protect: True},
|
||||
direct_connect_domain=DIRECT_CONNECT_DOMAIN,
|
||||
)
|
||||
|
||||
UNIFI_DISCOVERY_NO_MAC = UnifiDevice(
|
||||
source_ip=DEVICE_IP_ADDRESS,
|
||||
hw_addr=None,
|
||||
platform=DEVICE_HOSTNAME,
|
||||
hostname=DEVICE_HOSTNAME,
|
||||
services={UnifiService.Protect: True},
|
||||
direct_connect_domain=DIRECT_CONNECT_DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
def _patch_discovery(
|
||||
device: UnifiDevice | None = None, no_device: bool = False
|
||||
) -> Generator[MagicMock]:
|
||||
mock_aio_discovery = MagicMock(spec=AIOUnifiScanner)
|
||||
scanner_return = [] if no_device else [device or UNIFI_DISCOVERY_PROTECT]
|
||||
mock_aio_discovery.async_scan = AsyncMock(return_value=scanner_return)
|
||||
mock_aio_discovery.found_devices = scanner_return
|
||||
|
||||
@contextmanager
|
||||
def _patcher():
|
||||
with patch(
|
||||
"homeassistant.components.unifi_discovery.discovery.AIOUnifiScanner",
|
||||
return_value=mock_aio_discovery,
|
||||
):
|
||||
yield mock_aio_discovery
|
||||
|
||||
return _patcher()
|
||||
98
tests/components/unifi_discovery/test_config_flow.py
Normal file
98
tests/components/unifi_discovery/test_config_flow.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Test the UniFi Discovery config flow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.unifi_discovery.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo
|
||||
|
||||
from . import DEVICE_HOSTNAME, DEVICE_IP_ADDRESS, DEVICE_MAC_ADDRESS, _patch_discovery
|
||||
|
||||
DHCP_DISCOVERY = DhcpServiceInfo(
|
||||
hostname=DEVICE_HOSTNAME,
|
||||
ip=DEVICE_IP_ADDRESS,
|
||||
macaddress=DEVICE_MAC_ADDRESS.lower().replace(":", ""),
|
||||
)
|
||||
|
||||
SSDP_DISCOVERY = SsdpServiceInfo(
|
||||
ssdp_usn="mock_usn",
|
||||
ssdp_st="mock_st",
|
||||
upnp={
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("source", "data"),
|
||||
[
|
||||
(config_entries.SOURCE_DHCP, DHCP_DISCOVERY),
|
||||
(config_entries.SOURCE_SSDP, SSDP_DISCOVERY),
|
||||
],
|
||||
)
|
||||
async def test_dhcp_ssdp_abort_with_discovery_started(
|
||||
hass: HomeAssistant, source: str, data: DhcpServiceInfo | SsdpServiceInfo
|
||||
) -> None:
|
||||
"""Test DHCP and SSDP discovery triggers scanner and aborts."""
|
||||
with _patch_discovery() as mock_scanner:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": source},
|
||||
data=data,
|
||||
)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "discovery_started"
|
||||
assert mock_scanner.async_scan.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("source", "data"),
|
||||
[
|
||||
(config_entries.SOURCE_DHCP, DHCP_DISCOVERY),
|
||||
(config_entries.SOURCE_SSDP, SSDP_DISCOVERY),
|
||||
],
|
||||
)
|
||||
async def test_dhcp_ssdp_abort_already_in_progress(
|
||||
hass: HomeAssistant, source: str, data: DhcpServiceInfo | SsdpServiceInfo
|
||||
) -> None:
|
||||
"""Test DHCP and SSDP abort when another flow is already in progress."""
|
||||
with (
|
||||
_patch_discovery(),
|
||||
patch(
|
||||
"homeassistant.components.unifi_discovery.config_flow.UnifiDiscoveryFlowHandler._async_in_progress",
|
||||
return_value=[{"flow_id": "mock_flow"}],
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": source},
|
||||
data=data,
|
||||
)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_in_progress"
|
||||
|
||||
|
||||
async def test_user_flow_aborts(hass: HomeAssistant) -> None:
|
||||
"""Test user-initiated flow aborts."""
|
||||
with _patch_discovery() as mock_scanner:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "discovery_started"
|
||||
assert mock_scanner.async_scan.call_count == 1
|
||||
56
tests/components/unifi_discovery/test_init.py
Normal file
56
tests/components/unifi_discovery/test_init.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Test the UniFi Discovery init."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.unifi_discovery.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import UNIFI_DISCOVERY_NO_MAC, _patch_discovery
|
||||
|
||||
|
||||
async def test_setup_starts_discovery(hass: HomeAssistant) -> None:
|
||||
"""Test that async_setup starts discovery and dispatches flows."""
|
||||
with _patch_discovery():
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# The scanner should have dispatched a discovery flow for the Protect consumer
|
||||
flows = hass.config_entries.flow.async_progress_by_handler("unifiprotect")
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["context"]["source"] == config_entries.SOURCE_INTEGRATION_DISCOVERY
|
||||
|
||||
|
||||
async def test_setup_no_devices(hass: HomeAssistant) -> None:
|
||||
"""Test setup with no devices found."""
|
||||
with _patch_discovery(no_device=True):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
flows = hass.config_entries.flow.async_progress_by_handler("unifiprotect")
|
||||
assert len(flows) == 0
|
||||
|
||||
|
||||
async def test_setup_device_without_mac(hass: HomeAssistant) -> None:
|
||||
"""Test that devices without hw_addr are skipped."""
|
||||
with _patch_discovery(device=UNIFI_DISCOVERY_NO_MAC):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
flows = hass.config_entries.flow.async_progress_by_handler("unifiprotect")
|
||||
assert len(flows) == 0
|
||||
|
||||
|
||||
async def test_dependency_loads_discovery(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that loading unifiprotect triggers unifi_discovery as dependency."""
|
||||
with _patch_discovery():
|
||||
assert await async_setup_component(hass, "unifiprotect", {})
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# unifi_discovery should have been loaded as a dependency and started scanning
|
||||
flows = hass.config_entries.flow.async_progress_by_handler("unifiprotect")
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["context"]["source"] == config_entries.SOURCE_INTEGRATION_DISCOVERY
|
||||
@@ -43,7 +43,7 @@ def _patch_discovery(device=None, no_device=False):
|
||||
@contextmanager
|
||||
def _patcher():
|
||||
with patch(
|
||||
"homeassistant.components.unifiprotect.discovery.AIOUnifiScanner",
|
||||
"homeassistant.components.unifi_discovery.discovery.AIOUnifiScanner",
|
||||
return_value=mock_aio_discovery,
|
||||
):
|
||||
yield
|
||||
|
||||
@@ -60,6 +60,13 @@ DEFAULT_PASSWORD = "test-password"
|
||||
DEFAULT_API_KEY = "test-api-key"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_discovery():
|
||||
"""Prevent real network scanning in all unifiprotect tests."""
|
||||
with _patch_discovery(no_device=True):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="nvr")
|
||||
def mock_nvr():
|
||||
"""Mock UniFi Protect Camera device."""
|
||||
@@ -158,7 +165,6 @@ def mock_entry(
|
||||
"""Mock ProtectApiClient for testing."""
|
||||
|
||||
with (
|
||||
_patch_discovery(no_device=True),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.utils.ProtectApiClient"
|
||||
) as mock_api,
|
||||
|
||||
@@ -30,8 +30,6 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo
|
||||
|
||||
from . import (
|
||||
DEVICE_HOSTNAME,
|
||||
@@ -40,7 +38,6 @@ from . import (
|
||||
DIRECT_CONNECT_DOMAIN,
|
||||
UNIFI_DISCOVERY,
|
||||
UNIFI_DISCOVERY_PARTIAL,
|
||||
_patch_discovery,
|
||||
)
|
||||
from .conftest import (
|
||||
DEFAULT_API_KEY,
|
||||
@@ -54,24 +51,6 @@ from .conftest import (
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
DHCP_DISCOVERY = DhcpServiceInfo(
|
||||
hostname=DEVICE_HOSTNAME,
|
||||
ip=DEVICE_IP_ADDRESS,
|
||||
macaddress=DEVICE_MAC_ADDRESS.lower().replace(":", ""),
|
||||
)
|
||||
SSDP_DISCOVERY = (
|
||||
SsdpServiceInfo(
|
||||
ssdp_usn="mock_usn",
|
||||
ssdp_st="mock_st",
|
||||
ssdp_location=f"http://{DEVICE_IP_ADDRESS}:41417/rootDesc.xml",
|
||||
upnp={
|
||||
"friendlyName": "UniFi Dream Machine",
|
||||
"modelDescription": "UniFi Dream Machine Pro",
|
||||
"serialNumber": DEVICE_MAC_ADDRESS,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# Base user input without credentials (for tests that override them)
|
||||
BASE_USER_INPUT = {
|
||||
CONF_HOST: DEFAULT_HOST,
|
||||
@@ -563,8 +542,6 @@ async def test_form_options(
|
||||
ufp_config_entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
_patch_discovery(),
|
||||
patch("homeassistant.components.unifiprotect.async_start_discovery"),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.utils.ProtectApiClient"
|
||||
) as mock_api,
|
||||
@@ -600,42 +577,17 @@ async def test_form_options(
|
||||
await hass.config_entries.async_unload(ufp_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("source", "data"),
|
||||
[
|
||||
(config_entries.SOURCE_DHCP, DHCP_DISCOVERY),
|
||||
(config_entries.SOURCE_SSDP, SSDP_DISCOVERY),
|
||||
],
|
||||
)
|
||||
async def test_discovered_by_ssdp_or_dhcp(
|
||||
hass: HomeAssistant, source: str, data: DhcpServiceInfo | SsdpServiceInfo
|
||||
) -> None:
|
||||
"""Test we handoff to unifi-discovery when discovered via ssdp or dhcp."""
|
||||
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": source},
|
||||
data=data,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "discovery_started"
|
||||
|
||||
|
||||
async def test_discovered_by_unifi_discovery_direct_connect(
|
||||
hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR
|
||||
) -> None:
|
||||
"""Test a discovery from unifi-discovery."""
|
||||
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
@@ -714,13 +666,12 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated(
|
||||
)
|
||||
mock_config.add_to_hass(hass)
|
||||
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
@@ -747,7 +698,6 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin
|
||||
mock_config.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
_patch_discovery(),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.async_console_is_alive",
|
||||
return_value=False,
|
||||
@@ -785,7 +735,6 @@ async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_
|
||||
mock_config.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
_patch_discovery(),
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.config_flow.async_console_is_alive",
|
||||
return_value=True,
|
||||
@@ -821,13 +770,12 @@ async def test_discovered_host_not_updated_if_existing_is_a_hostname(
|
||||
)
|
||||
mock_config.add_to_hass(hass)
|
||||
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
@@ -839,13 +787,12 @@ async def test_discovered_by_unifi_discovery(
|
||||
) -> None:
|
||||
"""Test a discovery from unifi-discovery."""
|
||||
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
@@ -909,13 +856,12 @@ async def test_discovered_by_unifi_discovery_partial(
|
||||
) -> None:
|
||||
"""Test a discovery from unifi-discovery partial."""
|
||||
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT_PARTIAL,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT_PARTIAL,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
@@ -993,13 +939,12 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
)
|
||||
mock_config.add_to_hass(hass)
|
||||
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
@@ -1024,13 +969,12 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
)
|
||||
mock_config.add_to_hass(hass)
|
||||
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
@@ -1060,7 +1004,6 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
other_ip_dict["direct_connect_domain"] = "nomatchsameip.ui.direct"
|
||||
|
||||
with (
|
||||
_patch_discovery(),
|
||||
patch.object(
|
||||
hass.loop,
|
||||
"getaddrinfo",
|
||||
@@ -1103,7 +1046,6 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
other_ip_dict["direct_connect_domain"] = "nomatchsameip.ui.direct"
|
||||
|
||||
with (
|
||||
_patch_discovery(),
|
||||
patch.object(hass.loop, "getaddrinfo", side_effect=OSError),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -1193,7 +1135,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa
|
||||
other_ip_dict["source_ip"] = "127.0.0.2"
|
||||
other_ip_dict["direct_connect_domain"] = "y.ui.direct"
|
||||
|
||||
with _patch_discovery(), patch.object(hass.loop, "getaddrinfo", return_value=[]):
|
||||
with patch.object(hass.loop, "getaddrinfo", return_value=[]):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
@@ -1214,13 +1156,12 @@ async def test_discovery_can_be_ignored(hass: HomeAssistant) -> None:
|
||||
source=config_entries.SOURCE_IGNORE,
|
||||
)
|
||||
mock_config.add_to_hass(hass)
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
@@ -1256,13 +1197,12 @@ async def test_discovery_with_both_ignored_and_normal_entry(
|
||||
# Discovery should:
|
||||
# 1. Skip all ignored entries with different MAC (line 182 - continue)
|
||||
# 2. Continue to discovery flow since no matching entries
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Flow continues to discovery step since no match found
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
@@ -1312,13 +1252,12 @@ async def test_discovery_confirm_fallback_to_ip(
|
||||
mock_api_meta_info: Mock,
|
||||
) -> None:
|
||||
"""Test discovery confirm falls back to IP when direct connect fails."""
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
@@ -1363,13 +1302,12 @@ async def test_discovery_confirm_with_api_key_error(
|
||||
mock_api_meta_info: Mock,
|
||||
) -> None:
|
||||
"""Test discovery confirm preserves API key in form data on error."""
|
||||
with _patch_discovery():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=UNIFI_DISCOVERY_DICT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
|
||||
@@ -328,7 +328,7 @@ async def test_setup_failed_auth(hass: HomeAssistant, ufp: MockUFPFixture) -> No
|
||||
async def test_setup_starts_discovery(
|
||||
hass: HomeAssistant, ufp_config_entry: ConfigEntry, ufp_client: ProtectApiClient
|
||||
) -> None:
|
||||
"""Test setting up will start discovery."""
|
||||
"""Test setting up will start discovery via unifi_discovery dependency."""
|
||||
with (
|
||||
_patch_discovery(),
|
||||
patch(
|
||||
@@ -340,9 +340,9 @@ async def test_setup_starts_discovery(
|
||||
ufp = MockUFPFixture(ufp_config_entry, ufp_client)
|
||||
|
||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert ufp.entry.state is ConfigEntryState.LOADED
|
||||
await hass.async_block_till_done()
|
||||
# Discovery is now handled by unifi_discovery dependency
|
||||
assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1
|
||||
|
||||
|
||||
@@ -586,7 +586,6 @@ async def test_migrate_entry_version_2(hass: HomeAssistant) -> None:
|
||||
patch(
|
||||
"homeassistant.components.unifiprotect.async_setup_entry", return_value=True
|
||||
),
|
||||
patch("homeassistant.components.unifiprotect.async_start_discovery"),
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for the intent helpers."""
|
||||
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -983,3 +984,76 @@ async def test_get_all_entity_aliases(
|
||||
|
||||
state = State("light.test", "on", {"friendly_name": friendly_name})
|
||||
assert intent.async_get_entity_aliases(hass, entry, state=state) == expected
|
||||
|
||||
|
||||
async def test_intent_response_dict() -> None:
|
||||
"""Test that IntentResponse.as_dict() copies mutable objects."""
|
||||
response = intent.IntentResponse(
|
||||
language="en",
|
||||
intent=None,
|
||||
)
|
||||
# Prepare the intent response initial state
|
||||
response.async_set_speech(
|
||||
speech="Hello", speech_type="plain", extra_data={"key": "value"}
|
||||
)
|
||||
response.async_set_reprompt(
|
||||
speech="Hi", speech_type="plain", extra_data={"key2": "value2"}
|
||||
)
|
||||
response.async_set_card(title="Title", content="Content", card_type="simple")
|
||||
response.async_set_results(
|
||||
success_results=[
|
||||
intent.IntentResponseTarget(
|
||||
type=intent.IntentResponseTargetType.FLOOR,
|
||||
name="first floor",
|
||||
id="floor-1",
|
||||
)
|
||||
],
|
||||
failed_results=[
|
||||
intent.IntentResponseTarget(
|
||||
type=intent.IntentResponseTargetType.ENTITY,
|
||||
name="kitchen light",
|
||||
id="light.kitchen",
|
||||
)
|
||||
],
|
||||
)
|
||||
response.async_set_states(
|
||||
matched_states=[State("light.kitchen", "on")],
|
||||
unmatched_states=[State("light.bedroom", "off")],
|
||||
)
|
||||
response.async_set_speech_slots({"name": {"value": "kitchen"}})
|
||||
|
||||
response_dict1 = response.as_dict()
|
||||
response_dict2 = deepcopy(response_dict1)
|
||||
|
||||
# Mutate the original object
|
||||
response.async_set_speech(
|
||||
speech="Changed", speech_type="plain", extra_data={"key": "changed"}
|
||||
)
|
||||
response.async_set_reprompt(
|
||||
speech="Changed", speech_type="plain", extra_data={"key2": "changed2"}
|
||||
)
|
||||
response.async_set_card(title="Changed", content="Changed", card_type="simple")
|
||||
response.async_set_results(
|
||||
success_results=[
|
||||
intent.IntentResponseTarget(
|
||||
type=intent.IntentResponseTargetType.FLOOR,
|
||||
name="changed floor",
|
||||
id="floor-changed",
|
||||
)
|
||||
],
|
||||
failed_results=[
|
||||
intent.IntentResponseTarget(
|
||||
type=intent.IntentResponseTargetType.ENTITY,
|
||||
name="changed light",
|
||||
id="light.changed",
|
||||
)
|
||||
],
|
||||
)
|
||||
response.async_set_states(
|
||||
matched_states=[State("light.changed", "on")],
|
||||
unmatched_states=[State("light.changed_bedroom", "off")],
|
||||
)
|
||||
response.async_set_speech_slots({"name": {"value": "changed"}})
|
||||
|
||||
# The original dict should not be affected by the mutations
|
||||
assert response_dict1 == response_dict2
|
||||
|
||||
Reference in New Issue
Block a user