mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
2025.5.2 (#145072)
Co-authored-by: Shay Levy <levyshay1@gmail.com> Co-authored-by: Allen Porter <allen.porter@gmail.com> Co-authored-by: Franck Nijhof <git@frenck.dev> Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: TimL <tl@smlight.tech> Co-authored-by: Seweryn Zeman <seweryn.zeman@jazzy.pro> Co-authored-by: hahn-th <15319212+hahn-th@users.noreply.github.com> Co-authored-by: Luke Lashley <conway220@gmail.com> Co-authored-by: starkillerOG <starkiller.og@gmail.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Co-authored-by: Josef Zweck <josef@zweck.dev> Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> Co-authored-by: Ruben van Dijk <15885455+RubenNL@users.noreply.github.com> Co-authored-by: G Johansson <goran.johansson@shiftit.se> Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com> Co-authored-by: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Co-authored-by: Øyvind Matheson Wergeland <oyvind@wergeland.org> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> Co-authored-by: Brett Adams <Bre77@users.noreply.github.com> Co-authored-by: rjblake <richard.blake@gmail.com> Co-authored-by: Daniel Hjelseth Høyer <github@dahoiv.net> Co-authored-by: Matthias Alphart <farmio@alphart.net> Co-authored-by: Erik Montnemery <erik@montnemery.com> Co-authored-by: Robert Resch <robert@resch.dev> Co-authored-by: Odd Stråbø <oddstr13@openshell.no> Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> Co-authored-by: Bram Kragten <mail@bramkragten.nl> fix privacy mode availability for NVR IPC cams (#144569) fix enphase_envoy diagnostics home endpoint name (#144634) Close Octoprint aiohttp session on unload (#144670) Fix strings typo for Comelit (#144672) Fix wrong state in Husqvarna Automower (#144684) Fix Netgear handeling of missing MAC in device registry (#144722) Fix blocking call in azure storage (#144803) Fix Z-Wave unique id after controller reset (#144813) Fix blocking call in azure_storage config flow (#144818) Fix wall connector states in Teslemetry (#144855) Fix Reolink setup when ONVIF push is unsupported (#144869) Fix some Home Connect translation strings (#144905) Fix unknown Pure AQI in Sensibo (#144924) Fix Home Assistant Yellow config entry data (#144948) Fix ESPHome entities unavailable if deep sleep enabled after entry setup (#144970) fix from ZHA event `unique_id` (#145006) Fix climate idle state for Comelit (#145059) Fix fan AC mode in SmartThings AC (#145064) Fix Ecovacs mower area sensors (#145071)
This commit is contained in:
commit
f66feabaaf
@ -39,13 +39,22 @@ async def async_setup_entry(
|
|||||||
session = async_create_clientsession(
|
session = async_create_clientsession(
|
||||||
hass, timeout=ClientTimeout(connect=10, total=12 * 60 * 60)
|
hass, timeout=ClientTimeout(connect=10, total=12 * 60 * 60)
|
||||||
)
|
)
|
||||||
container_client = ContainerClient(
|
|
||||||
|
def create_container_client() -> ContainerClient:
|
||||||
|
"""Create a ContainerClient."""
|
||||||
|
|
||||||
|
return ContainerClient(
|
||||||
account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
|
account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
|
||||||
container_name=entry.data[CONF_CONTAINER_NAME],
|
container_name=entry.data[CONF_CONTAINER_NAME],
|
||||||
credential=entry.data[CONF_STORAGE_ACCOUNT_KEY],
|
credential=entry.data[CONF_STORAGE_ACCOUNT_KEY],
|
||||||
transport=AioHttpTransport(session=session),
|
transport=AioHttpTransport(session=session),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# has a blocking call to open in cpython
|
||||||
|
container_client: ContainerClient = await hass.async_add_executor_job(
|
||||||
|
create_container_client
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not await container_client.exists():
|
if not await container_client.exists():
|
||||||
await container_client.create_container()
|
await container_client.create_container()
|
||||||
|
@ -27,9 +27,25 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for azure storage."""
|
"""Handle a config flow for azure storage."""
|
||||||
|
|
||||||
def get_account_url(self, account_name: str) -> str:
|
async def get_container_client(
|
||||||
"""Get the account URL."""
|
self, account_name: str, container_name: str, storage_account_key: str
|
||||||
return f"https://{account_name}.blob.core.windows.net/"
|
) -> ContainerClient:
|
||||||
|
"""Get the container client.
|
||||||
|
|
||||||
|
ContainerClient has a blocking call to open in cpython
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = async_get_clientsession(self.hass)
|
||||||
|
|
||||||
|
def create_container_client() -> ContainerClient:
|
||||||
|
return ContainerClient(
|
||||||
|
account_url=f"https://{account_name}.blob.core.windows.net/",
|
||||||
|
container_name=container_name,
|
||||||
|
credential=storage_account_key,
|
||||||
|
transport=AioHttpTransport(session=session),
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self.hass.async_add_executor_job(create_container_client)
|
||||||
|
|
||||||
async def validate_config(
|
async def validate_config(
|
||||||
self, container_client: ContainerClient
|
self, container_client: ContainerClient
|
||||||
@ -58,11 +74,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self._async_abort_entries_match(
|
self._async_abort_entries_match(
|
||||||
{CONF_ACCOUNT_NAME: user_input[CONF_ACCOUNT_NAME]}
|
{CONF_ACCOUNT_NAME: user_input[CONF_ACCOUNT_NAME]}
|
||||||
)
|
)
|
||||||
container_client = ContainerClient(
|
container_client = await self.get_container_client(
|
||||||
account_url=self.get_account_url(user_input[CONF_ACCOUNT_NAME]),
|
account_name=user_input[CONF_ACCOUNT_NAME],
|
||||||
container_name=user_input[CONF_CONTAINER_NAME],
|
container_name=user_input[CONF_CONTAINER_NAME],
|
||||||
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||||
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
|
|
||||||
)
|
)
|
||||||
errors = await self.validate_config(container_client)
|
errors = await self.validate_config(container_client)
|
||||||
|
|
||||||
@ -99,12 +114,12 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
reauth_entry = self._get_reauth_entry()
|
reauth_entry = self._get_reauth_entry()
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
container_client = ContainerClient(
|
container_client = await self.get_container_client(
|
||||||
account_url=self.get_account_url(reauth_entry.data[CONF_ACCOUNT_NAME]),
|
account_name=reauth_entry.data[CONF_ACCOUNT_NAME],
|
||||||
container_name=reauth_entry.data[CONF_CONTAINER_NAME],
|
container_name=reauth_entry.data[CONF_CONTAINER_NAME],
|
||||||
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||||
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
errors = await self.validate_config(container_client)
|
errors = await self.validate_config(container_client)
|
||||||
if not errors:
|
if not errors:
|
||||||
return self.async_update_reload_and_abort(
|
return self.async_update_reload_and_abort(
|
||||||
@ -129,13 +144,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
reconfigure_entry = self._get_reconfigure_entry()
|
reconfigure_entry = self._get_reconfigure_entry()
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
container_client = ContainerClient(
|
container_client = await self.get_container_client(
|
||||||
account_url=self.get_account_url(
|
account_name=reconfigure_entry.data[CONF_ACCOUNT_NAME],
|
||||||
reconfigure_entry.data[CONF_ACCOUNT_NAME]
|
|
||||||
),
|
|
||||||
container_name=user_input[CONF_CONTAINER_NAME],
|
container_name=user_input[CONF_CONTAINER_NAME],
|
||||||
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||||
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
|
|
||||||
)
|
)
|
||||||
errors = await self.validate_config(container_client)
|
errors = await self.validate_config(container_client)
|
||||||
if not errors:
|
if not errors:
|
||||||
|
@ -77,6 +77,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ComelitConfigEntry) ->
|
|||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
|
||||||
await coordinator.api.logout()
|
await coordinator.api.logout()
|
||||||
await coordinator.api.close()
|
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
@ -134,11 +134,9 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
|
|||||||
self._attr_current_temperature = values[0] / 10
|
self._attr_current_temperature = values[0] / 10
|
||||||
|
|
||||||
self._attr_hvac_action = None
|
self._attr_hvac_action = None
|
||||||
if _mode == ClimaComelitMode.OFF:
|
|
||||||
self._attr_hvac_action = HVACAction.OFF
|
|
||||||
if not _active:
|
if not _active:
|
||||||
self._attr_hvac_action = HVACAction.IDLE
|
self._attr_hvac_action = HVACAction.IDLE
|
||||||
if _mode in API_STATUS:
|
elif _mode in API_STATUS:
|
||||||
self._attr_hvac_action = API_STATUS[_mode]["hvac_action"]
|
self._attr_hvac_action = API_STATUS[_mode]["hvac_action"]
|
||||||
|
|
||||||
self._attr_hvac_mode = None
|
self._attr_hvac_mode = None
|
||||||
|
@ -73,7 +73,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
|||||||
) from err
|
) from err
|
||||||
finally:
|
finally:
|
||||||
await api.logout()
|
await api.logout()
|
||||||
await api.close()
|
|
||||||
|
|
||||||
return {"title": data[CONF_HOST]}
|
return {"title": data[CONF_HOST]}
|
||||||
|
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aiocomelit"],
|
"loggers": ["aiocomelit"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["aiocomelit==0.12.0"]
|
"requirements": ["aiocomelit==0.12.1"]
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@
|
|||||||
"cannot_authenticate": {
|
"cannot_authenticate": {
|
||||||
"message": "Error authenticating"
|
"message": "Error authenticating"
|
||||||
},
|
},
|
||||||
"updated_failed": {
|
"update_failed": {
|
||||||
"message": "Failed to update data: {error}"
|
"message": "Failed to update data: {error}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aiodhcpwatcher==1.1.1",
|
"aiodhcpwatcher==1.1.1",
|
||||||
"aiodiscover==2.6.1",
|
"aiodiscover==2.7.0",
|
||||||
"cached-ipaddress==0.10.0"
|
"cached-ipaddress==0.10.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||||
"requirements": ["py-sucks==0.9.10", "deebot-client==13.1.0"]
|
"requirements": ["py-sucks==0.9.10", "deebot-client==13.2.0"]
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,8 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Generic
|
from typing import Any, Generic
|
||||||
|
|
||||||
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan
|
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
|
||||||
|
from deebot_client.device import Device
|
||||||
from deebot_client.events import (
|
from deebot_client.events import (
|
||||||
BatteryEvent,
|
BatteryEvent,
|
||||||
ErrorEvent,
|
ErrorEvent,
|
||||||
@ -34,7 +35,7 @@ from homeassistant.const import (
|
|||||||
UnitOfArea,
|
UnitOfArea,
|
||||||
UnitOfTime,
|
UnitOfTime,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
@ -59,6 +60,15 @@ class EcovacsSensorEntityDescription(
|
|||||||
"""Ecovacs sensor entity description."""
|
"""Ecovacs sensor entity description."""
|
||||||
|
|
||||||
value_fn: Callable[[EventT], StateType]
|
value_fn: Callable[[EventT], StateType]
|
||||||
|
native_unit_of_measurement_fn: Callable[[DeviceType], str | None] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
|
||||||
|
"""Get the area native unit of measurement based on device type."""
|
||||||
|
if device_type is DeviceType.MOWER:
|
||||||
|
return UnitOfArea.SQUARE_CENTIMETERS
|
||||||
|
return UnitOfArea.SQUARE_METERS
|
||||||
|
|
||||||
|
|
||||||
ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
|
ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
|
||||||
@ -68,7 +78,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
|
|||||||
capability_fn=lambda caps: caps.stats.clean,
|
capability_fn=lambda caps: caps.stats.clean,
|
||||||
value_fn=lambda e: e.area,
|
value_fn=lambda e: e.area,
|
||||||
translation_key="stats_area",
|
translation_key="stats_area",
|
||||||
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
|
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
|
||||||
),
|
),
|
||||||
EcovacsSensorEntityDescription[StatsEvent](
|
EcovacsSensorEntityDescription[StatsEvent](
|
||||||
key="stats_time",
|
key="stats_time",
|
||||||
@ -85,7 +95,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
|
|||||||
value_fn=lambda e: e.area,
|
value_fn=lambda e: e.area,
|
||||||
key="total_stats_area",
|
key="total_stats_area",
|
||||||
translation_key="total_stats_area",
|
translation_key="total_stats_area",
|
||||||
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
|
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
),
|
),
|
||||||
EcovacsSensorEntityDescription[TotalStatsEvent](
|
EcovacsSensorEntityDescription[TotalStatsEvent](
|
||||||
@ -249,6 +259,27 @@ class EcovacsSensor(
|
|||||||
|
|
||||||
entity_description: EcovacsSensorEntityDescription
|
entity_description: EcovacsSensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device: Device,
|
||||||
|
capability: CapabilityEvent,
|
||||||
|
entity_description: EcovacsSensorEntityDescription,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize entity."""
|
||||||
|
super().__init__(device, capability, entity_description, **kwargs)
|
||||||
|
if (
|
||||||
|
entity_description.native_unit_of_measurement_fn
|
||||||
|
and (
|
||||||
|
native_unit_of_measurement
|
||||||
|
:= entity_description.native_unit_of_measurement_fn(
|
||||||
|
device.capabilities.device_type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
is not None
|
||||||
|
):
|
||||||
|
self._attr_native_unit_of_measurement = native_unit_of_measurement
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Set up the event listeners now that hass is ready."""
|
"""Set up the event listeners now that hass is ready."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
@ -64,7 +64,7 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
|
|||||||
"/ivp/ensemble/generator",
|
"/ivp/ensemble/generator",
|
||||||
"/ivp/meters",
|
"/ivp/meters",
|
||||||
"/ivp/meters/readings",
|
"/ivp/meters/readings",
|
||||||
"/home,",
|
"/home",
|
||||||
]
|
]
|
||||||
|
|
||||||
for end_point in end_points:
|
for end_point in end_points:
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyenphase"],
|
"loggers": ["pyenphase"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pyenphase==1.26.0"],
|
"requirements": ["pyenphase==1.26.1"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_enphase-envoy._tcp.local."
|
"type": "_enphase-envoy._tcp.local."
|
||||||
|
@ -223,7 +223,6 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
|||||||
self._states = cast(dict[int, _StateT], entry_data.state[state_type])
|
self._states = cast(dict[int, _StateT], entry_data.state[state_type])
|
||||||
assert entry_data.device_info is not None
|
assert entry_data.device_info is not None
|
||||||
device_info = entry_data.device_info
|
device_info = entry_data.device_info
|
||||||
self._device_info = device_info
|
|
||||||
self._on_entry_data_changed()
|
self._on_entry_data_changed()
|
||||||
self._key = entity_info.key
|
self._key = entity_info.key
|
||||||
self._state_type = state_type
|
self._state_type = state_type
|
||||||
@ -311,6 +310,11 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
|||||||
@callback
|
@callback
|
||||||
def _on_entry_data_changed(self) -> None:
|
def _on_entry_data_changed(self) -> None:
|
||||||
entry_data = self._entry_data
|
entry_data = self._entry_data
|
||||||
|
# Update the device info since it can change
|
||||||
|
# when the device is reconnected
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert entry_data.device_info is not None
|
||||||
|
self._device_info = entry_data.device_info
|
||||||
self._api_version = entry_data.api_version
|
self._api_version = entry_data.api_version
|
||||||
self._client = entry_data.client
|
self._client = entry_data.client
|
||||||
if self._device_info.has_deep_sleep:
|
if self._device_info.has_deep_sleep:
|
||||||
|
@ -35,7 +35,7 @@ async def validate_host(
|
|||||||
hass: HomeAssistant, host: str
|
hass: HomeAssistant, host: str
|
||||||
) -> tuple[str, FroniusConfigEntryData]:
|
) -> tuple[str, FroniusConfigEntryData]:
|
||||||
"""Validate the user input allows us to connect."""
|
"""Validate the user input allows us to connect."""
|
||||||
fronius = Fronius(async_get_clientsession(hass), host)
|
fronius = Fronius(async_get_clientsession(hass, verify_ssl=False), host)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
datalogger_info: dict[str, Any]
|
datalogger_info: dict[str, Any]
|
||||||
|
@ -20,5 +20,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20250509.0"]
|
"requirements": ["home-assistant-frontend==20250516.0"]
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["googleapiclient"],
|
"loggers": ["googleapiclient"],
|
||||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.0"]
|
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.4"]
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["holidays==0.70", "babel==2.15.0"]
|
"requirements": ["holidays==0.72", "babel==2.15.0"]
|
||||||
}
|
}
|
||||||
|
@ -234,7 +234,7 @@
|
|||||||
"consumer_products_coffee_maker_program_coffee_world_black_eye": "Black eye",
|
"consumer_products_coffee_maker_program_coffee_world_black_eye": "Black eye",
|
||||||
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "Dead eye",
|
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "Dead eye",
|
||||||
"consumer_products_coffee_maker_program_beverage_hot_water": "Hot water",
|
"consumer_products_coffee_maker_program_beverage_hot_water": "Hot water",
|
||||||
"dishcare_dishwasher_program_pre_rinse": "Pre_rinse",
|
"dishcare_dishwasher_program_pre_rinse": "Pre-rinse",
|
||||||
"dishcare_dishwasher_program_auto_1": "Auto 1",
|
"dishcare_dishwasher_program_auto_1": "Auto 1",
|
||||||
"dishcare_dishwasher_program_auto_2": "Auto 2",
|
"dishcare_dishwasher_program_auto_2": "Auto 2",
|
||||||
"dishcare_dishwasher_program_auto_3": "Auto 3",
|
"dishcare_dishwasher_program_auto_3": "Auto 3",
|
||||||
@ -252,7 +252,7 @@
|
|||||||
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
|
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
|
||||||
"dishcare_dishwasher_program_magic_daily": "Magic daily",
|
"dishcare_dishwasher_program_magic_daily": "Magic daily",
|
||||||
"dishcare_dishwasher_program_super_60": "Super 60ºC",
|
"dishcare_dishwasher_program_super_60": "Super 60ºC",
|
||||||
"dishcare_dishwasher_program_kurz_60": "Kurz 60ºC",
|
"dishcare_dishwasher_program_kurz_60": "Speed 60ºC",
|
||||||
"dishcare_dishwasher_program_express_sparkle_65": "Express sparkle 65ºC",
|
"dishcare_dishwasher_program_express_sparkle_65": "Express sparkle 65ºC",
|
||||||
"dishcare_dishwasher_program_machine_care": "Machine care",
|
"dishcare_dishwasher_program_machine_care": "Machine care",
|
||||||
"dishcare_dishwasher_program_steam_fresh": "Steam fresh",
|
"dishcare_dishwasher_program_steam_fresh": "Steam fresh",
|
||||||
|
@ -90,16 +90,17 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
minor_version=2,
|
minor_version=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
if config_entry.minor_version == 2:
|
if config_entry.minor_version <= 3:
|
||||||
# Add a `firmware_version` key
|
# Add a `firmware_version` key if it doesn't exist to handle entries created
|
||||||
|
# with minor version 1.3 where the firmware version was not set.
|
||||||
hass.config_entries.async_update_entry(
|
hass.config_entries.async_update_entry(
|
||||||
config_entry,
|
config_entry,
|
||||||
data={
|
data={
|
||||||
**config_entry.data,
|
**config_entry.data,
|
||||||
FIRMWARE_VERSION: None,
|
FIRMWARE_VERSION: config_entry.data.get(FIRMWARE_VERSION),
|
||||||
},
|
},
|
||||||
version=1,
|
version=1,
|
||||||
minor_version=3,
|
minor_version=4,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
|
@ -62,7 +62,7 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for Home Assistant Yellow."""
|
"""Handle a config flow for Home Assistant Yellow."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
MINOR_VERSION = 3
|
MINOR_VERSION = 4
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""Instantiate config flow."""
|
"""Instantiate config flow."""
|
||||||
@ -116,6 +116,11 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
|
|||||||
if self._probed_firmware_info is not None
|
if self._probed_firmware_info is not None
|
||||||
else ApplicationType.EZSP
|
else ApplicationType.EZSP
|
||||||
).value,
|
).value,
|
||||||
|
FIRMWARE_VERSION: (
|
||||||
|
self._probed_firmware_info.firmware_version
|
||||||
|
if self._probed_firmware_info is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
"""Support for HomematicIP Cloud events."""
|
"""Support for HomematicIP Cloud events."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from homematicip.base.channel_event import ChannelEvent
|
||||||
|
from homematicip.base.functionalChannels import FunctionalChannel
|
||||||
from homematicip.device import Device
|
from homematicip.device import Device
|
||||||
|
|
||||||
from homeassistant.components.event import (
|
from homeassistant.components.event import (
|
||||||
@ -23,6 +26,9 @@ from .hap import HomematicipHAP
|
|||||||
class HmipEventEntityDescription(EventEntityDescription):
|
class HmipEventEntityDescription(EventEntityDescription):
|
||||||
"""Description of a HomematicIP Cloud event."""
|
"""Description of a HomematicIP Cloud event."""
|
||||||
|
|
||||||
|
channel_event_types: list[str] | None = None
|
||||||
|
channel_selector_fn: Callable[[FunctionalChannel], bool] | None = None
|
||||||
|
|
||||||
|
|
||||||
EVENT_DESCRIPTIONS = {
|
EVENT_DESCRIPTIONS = {
|
||||||
"doorbell": HmipEventEntityDescription(
|
"doorbell": HmipEventEntityDescription(
|
||||||
@ -30,6 +36,8 @@ EVENT_DESCRIPTIONS = {
|
|||||||
translation_key="doorbell",
|
translation_key="doorbell",
|
||||||
device_class=EventDeviceClass.DOORBELL,
|
device_class=EventDeviceClass.DOORBELL,
|
||||||
event_types=["ring"],
|
event_types=["ring"],
|
||||||
|
channel_event_types=["DOOR_BELL_SENSOR_EVENT"],
|
||||||
|
channel_selector_fn=lambda channel: channel.channelRole == "DOOR_BELL_INPUT",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,24 +49,29 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the HomematicIP cover from a config entry."""
|
"""Set up the HomematicIP cover from a config entry."""
|
||||||
hap = hass.data[DOMAIN][config_entry.unique_id]
|
hap = hass.data[DOMAIN][config_entry.unique_id]
|
||||||
|
entities: list[HomematicipGenericEntity] = []
|
||||||
|
|
||||||
async_add_entities(
|
entities.extend(
|
||||||
HomematicipDoorBellEvent(
|
HomematicipDoorBellEvent(
|
||||||
hap,
|
hap,
|
||||||
device,
|
device,
|
||||||
channel.index,
|
channel.index,
|
||||||
EVENT_DESCRIPTIONS["doorbell"],
|
description,
|
||||||
)
|
)
|
||||||
|
for description in EVENT_DESCRIPTIONS.values()
|
||||||
for device in hap.home.devices
|
for device in hap.home.devices
|
||||||
for channel in device.functionalChannels
|
for channel in device.functionalChannels
|
||||||
if channel.channelRole == "DOOR_BELL_INPUT"
|
if description.channel_selector_fn and description.channel_selector_fn(channel)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
|
class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
|
||||||
"""Event class for HomematicIP doorbell events."""
|
"""Event class for HomematicIP doorbell events."""
|
||||||
|
|
||||||
_attr_device_class = EventDeviceClass.DOORBELL
|
_attr_device_class = EventDeviceClass.DOORBELL
|
||||||
|
entity_description: HmipEventEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -86,9 +99,27 @@ class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
|
|||||||
@callback
|
@callback
|
||||||
def _async_handle_event(self, *args, **kwargs) -> None:
|
def _async_handle_event(self, *args, **kwargs) -> None:
|
||||||
"""Handle the event fired by the functional channel."""
|
"""Handle the event fired by the functional channel."""
|
||||||
|
raised_channel_event = self._get_channel_event_from_args(*args)
|
||||||
|
|
||||||
|
if not self._should_raise(raised_channel_event):
|
||||||
|
return
|
||||||
|
|
||||||
event_types = self.entity_description.event_types
|
event_types = self.entity_description.event_types
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert event_types is not None
|
assert event_types is not None
|
||||||
|
|
||||||
self._trigger_event(event_type=event_types[0])
|
self._trigger_event(event_type=event_types[0])
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def _should_raise(self, event_type: str) -> bool:
|
||||||
|
"""Check if the event should be raised."""
|
||||||
|
if self.entity_description.channel_event_types is None:
|
||||||
|
return False
|
||||||
|
return event_type in self.entity_description.channel_event_types
|
||||||
|
|
||||||
|
def _get_channel_event_from_args(self, *args) -> str:
|
||||||
|
"""Get the channel event."""
|
||||||
|
if isinstance(args[0], ChannelEvent):
|
||||||
|
return args[0].channelEventType
|
||||||
|
|
||||||
|
return ""
|
||||||
|
@ -110,14 +110,14 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity):
|
|||||||
mower_attributes = self.mower_attributes
|
mower_attributes = self.mower_attributes
|
||||||
if mower_attributes.mower.state in PAUSED_STATES:
|
if mower_attributes.mower.state in PAUSED_STATES:
|
||||||
return LawnMowerActivity.PAUSED
|
return LawnMowerActivity.PAUSED
|
||||||
if mower_attributes.mower.state in MowerStates.IN_OPERATION:
|
|
||||||
if mower_attributes.mower.activity == MowerActivities.GOING_HOME:
|
|
||||||
return LawnMowerActivity.RETURNING
|
|
||||||
return LawnMowerActivity.MOWING
|
|
||||||
if (mower_attributes.mower.state == "RESTRICTED") or (
|
if (mower_attributes.mower.state == "RESTRICTED") or (
|
||||||
mower_attributes.mower.activity in DOCKED_ACTIVITIES
|
mower_attributes.mower.activity in DOCKED_ACTIVITIES
|
||||||
):
|
):
|
||||||
return LawnMowerActivity.DOCKED
|
return LawnMowerActivity.DOCKED
|
||||||
|
if mower_attributes.mower.state in MowerStates.IN_OPERATION:
|
||||||
|
if mower_attributes.mower.activity == MowerActivities.GOING_HOME:
|
||||||
|
return LawnMowerActivity.RETURNING
|
||||||
|
return LawnMowerActivity.MOWING
|
||||||
return LawnMowerActivity.ERROR
|
return LawnMowerActivity.ERROR
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -58,6 +58,7 @@ class INKBIRDActiveBluetoothProcessorCoordinator(
|
|||||||
update_method=self._async_on_update,
|
update_method=self._async_on_update,
|
||||||
needs_poll_method=self._async_needs_poll,
|
needs_poll_method=self._async_needs_poll,
|
||||||
poll_method=self._async_poll_data,
|
poll_method=self._async_poll_data,
|
||||||
|
connectable=False, # Polling only happens if active scanning is disabled
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_init(self) -> None:
|
async def async_init(self) -> None:
|
||||||
|
@ -37,5 +37,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pylamarzocco"],
|
"loggers": ["pylamarzocco"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pylamarzocco==2.0.1"]
|
"requirements": ["pylamarzocco==2.0.3"]
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,6 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["linkplay"],
|
"loggers": ["linkplay"],
|
||||||
"requirements": ["python-linkplay==0.2.4"],
|
"requirements": ["python-linkplay==0.2.5"],
|
||||||
"zeroconf": ["_linkplay._tcp.local."]
|
"zeroconf": ["_linkplay._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["ical"],
|
"loggers": ["ical"],
|
||||||
"requirements": ["ical==9.2.0"]
|
"requirements": ["ical==9.2.4"]
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["ical==9.2.0"]
|
"requirements": ["ical==9.2.4"]
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ from .const import DOMAIN
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
TWO_YEARS = 2 * 365 * 24
|
TWO_YEARS_DAYS = 2 * 365
|
||||||
|
|
||||||
|
|
||||||
class MillDataUpdateCoordinator(DataUpdateCoordinator):
|
class MillDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
@ -91,7 +91,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
if not last_stats or not last_stats.get(statistic_id):
|
if not last_stats or not last_stats.get(statistic_id):
|
||||||
hourly_data = (
|
hourly_data = (
|
||||||
await self.mill_data_connection.fetch_historic_energy_usage(
|
await self.mill_data_connection.fetch_historic_energy_usage(
|
||||||
dev_id, n_days=TWO_YEARS
|
dev_id, n_days=TWO_YEARS_DAYS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
hourly_data = dict(sorted(hourly_data.items(), key=lambda x: x[0]))
|
hourly_data = dict(sorted(hourly_data.items(), key=lambda x: x[0]))
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/mill",
|
"documentation": "https://www.home-assistant.io/integrations/mill",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["mill", "mill_local"],
|
"loggers": ["mill", "mill_local"],
|
||||||
"requirements": ["millheater==0.12.3", "mill-local==0.3.0"]
|
"requirements": ["millheater==0.12.5", "mill-local==0.3.0"]
|
||||||
}
|
}
|
||||||
|
@ -2063,7 +2063,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
entities = [
|
entities = [
|
||||||
SelectOptionDict(
|
SelectOptionDict(
|
||||||
value=key,
|
value=key,
|
||||||
label=f"{device_name} {component_data.get(CONF_NAME, '-')}"
|
label=f"{device_name} {component_data.get(CONF_NAME, '-') or '-'}"
|
||||||
f" ({component_data[CONF_PLATFORM]})",
|
f" ({component_data[CONF_PLATFORM]})",
|
||||||
)
|
)
|
||||||
for key, component_data in self._subentry_data["components"].items()
|
for key, component_data in self._subentry_data["components"].items()
|
||||||
@ -2295,7 +2295,8 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
self._component_id = None
|
self._component_id = None
|
||||||
mqtt_device = self._subentry_data[CONF_DEVICE][CONF_NAME]
|
mqtt_device = self._subentry_data[CONF_DEVICE][CONF_NAME]
|
||||||
mqtt_items = ", ".join(
|
mqtt_items = ", ".join(
|
||||||
f"{mqtt_device} {component_data.get(CONF_NAME, '-')} ({component_data[CONF_PLATFORM]})"
|
f"{mqtt_device} {component_data.get(CONF_NAME, '-') or '-'} "
|
||||||
|
f"({component_data[CONF_PLATFORM]})"
|
||||||
for component_data in self._subentry_data["components"].values()
|
for component_data in self._subentry_data["components"].values()
|
||||||
)
|
)
|
||||||
menu_options = [
|
menu_options = [
|
||||||
|
@ -150,7 +150,11 @@ class NetgearRouter:
|
|||||||
if device_entry.via_device_id is None:
|
if device_entry.via_device_id is None:
|
||||||
continue # do not add the router itself
|
continue # do not add the router itself
|
||||||
|
|
||||||
device_mac = dict(device_entry.connections)[dr.CONNECTION_NETWORK_MAC]
|
device_mac = dict(device_entry.connections).get(
|
||||||
|
dr.CONNECTION_NETWORK_MAC
|
||||||
|
)
|
||||||
|
if device_mac is None:
|
||||||
|
continue
|
||||||
self.devices[device_mac] = {
|
self.devices[device_mac] = {
|
||||||
"mac": device_mac,
|
"mac": device_mac,
|
||||||
"name": device_entry.name,
|
"name": device_entry.name,
|
||||||
|
@ -181,11 +181,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
session = aiohttp.ClientSession(connector=connector)
|
session = aiohttp.ClientSession(connector=connector)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_close_websession(event: Event) -> None:
|
def _async_close_websession(event: Event | None = None) -> None:
|
||||||
"""Close websession."""
|
"""Close websession."""
|
||||||
session.detach()
|
session.detach()
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close_websession)
|
entry.async_on_unload(_async_close_websession)
|
||||||
|
entry.async_on_unload(
|
||||||
|
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_close_websession)
|
||||||
|
)
|
||||||
|
|
||||||
client = OctoprintClient(
|
client = OctoprintClient(
|
||||||
host=entry.data[CONF_HOST],
|
host=entry.data[CONF_HOST],
|
||||||
|
@ -140,7 +140,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
content.append(
|
content.append(
|
||||||
ResponseInputImageParam(
|
ResponseInputImageParam(
|
||||||
type="input_image",
|
type="input_image",
|
||||||
file_id=filename,
|
|
||||||
image_url=f"data:{mime_type};base64,{base64_file}",
|
image_url=f"data:{mime_type};base64,{base64_file}",
|
||||||
detail="auto",
|
detail="auto",
|
||||||
)
|
)
|
||||||
|
@ -32,6 +32,8 @@ from .const import (
|
|||||||
PLACEHOLDER_WEBHOOK_URL,
|
PLACEHOLDER_WEBHOOK_URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AUTH_TOKEN_URL = "https://intercom.help/plaato/en/articles/5004720-auth_token"
|
||||||
|
|
||||||
|
|
||||||
class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN):
|
class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handles a Plaato config flow."""
|
"""Handles a Plaato config flow."""
|
||||||
@ -153,7 +155,10 @@ class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
step_id="api_method",
|
step_id="api_method",
|
||||||
data_schema=data_schema,
|
data_schema=data_schema,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders={PLACEHOLDER_DEVICE_TYPE: device_type.name},
|
description_placeholders={
|
||||||
|
PLACEHOLDER_DEVICE_TYPE: device_type.name,
|
||||||
|
"auth_token_url": AUTH_TOKEN_URL,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _get_webhook_id(self):
|
async def _get_webhook_id(self):
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"api_method": {
|
"api_method": {
|
||||||
"title": "Select API method",
|
"title": "Select API method",
|
||||||
"description": "To be able to query the API an `auth_token` is required which can be obtained by following [these](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructions\n\n Selected device: **{device_type}** \n\nIf you rather use the built in webhook method (Airlock only) please check the box below and leave Auth Token blank",
|
"description": "To be able to query the API an 'auth token' is required which can be obtained by following [these instructions]({auth_token_url})\n\nSelected device: **{device_type}** \n\nIf you prefer to use the built-in webhook method (Airlock only) please check the box below and leave 'Auth token' blank",
|
||||||
"data": {
|
"data": {
|
||||||
"use_webhook": "Use webhook",
|
"use_webhook": "Use webhook",
|
||||||
"token": "Paste Auth Token here"
|
"token": "Paste Auth Token here"
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["ical"],
|
"loggers": ["ical"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["ical==9.2.0"]
|
"requirements": ["ical==9.2.4"]
|
||||||
}
|
}
|
||||||
|
@ -364,7 +364,10 @@ def migrate_entity_ids(
|
|||||||
devices = dr.async_entries_for_config_entry(device_reg, config_entry_id)
|
devices = dr.async_entries_for_config_entry(device_reg, config_entry_id)
|
||||||
ch_device_ids = {}
|
ch_device_ids = {}
|
||||||
for device in devices:
|
for device in devices:
|
||||||
(device_uid, ch, is_chime) = get_device_uid_and_ch(device, host)
|
for dev_id in device.identifiers:
|
||||||
|
(device_uid, ch, is_chime) = get_device_uid_and_ch(dev_id, host)
|
||||||
|
if not device_uid:
|
||||||
|
continue
|
||||||
|
|
||||||
if host.api.supported(None, "UID") and device_uid[0] != host.unique_id:
|
if host.api.supported(None, "UID") and device_uid[0] != host.unique_id:
|
||||||
if ch is None:
|
if ch is None:
|
||||||
@ -372,33 +375,67 @@ def migrate_entity_ids(
|
|||||||
else:
|
else:
|
||||||
new_device_id = f"{host.unique_id}_{device_uid[1]}"
|
new_device_id = f"{host.unique_id}_{device_uid[1]}"
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Updating Reolink device UID from %s to %s", device_uid, new_device_id
|
"Updating Reolink device UID from %s to %s",
|
||||||
|
device_uid,
|
||||||
|
new_device_id,
|
||||||
)
|
)
|
||||||
new_identifiers = {(DOMAIN, new_device_id)}
|
new_identifiers = {(DOMAIN, new_device_id)}
|
||||||
device_reg.async_update_device(device.id, new_identifiers=new_identifiers)
|
device_reg.async_update_device(
|
||||||
|
device.id, new_identifiers=new_identifiers
|
||||||
|
)
|
||||||
|
|
||||||
if ch is None or is_chime:
|
if ch is None or is_chime:
|
||||||
continue # Do not consider the NVR itself or chimes
|
continue # Do not consider the NVR itself or chimes
|
||||||
|
|
||||||
|
# Check for wrongfully combined host with NVR entities in one device
|
||||||
|
# Can be removed in HA 2025.12
|
||||||
|
if (DOMAIN, host.unique_id) in device.identifiers:
|
||||||
|
new_identifiers = device.identifiers.copy()
|
||||||
|
for old_id in device.identifiers:
|
||||||
|
if old_id[0] == DOMAIN and old_id[1] != host.unique_id:
|
||||||
|
new_identifiers.remove(old_id)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Updating Reolink device identifiers from %s to %s",
|
||||||
|
device.identifiers,
|
||||||
|
new_identifiers,
|
||||||
|
)
|
||||||
|
device_reg.async_update_device(
|
||||||
|
device.id, new_identifiers=new_identifiers
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
# Check for wrongfully added MAC of the NVR/Hub to the camera
|
# Check for wrongfully added MAC of the NVR/Hub to the camera
|
||||||
# Can be removed in HA 2025.12
|
# Can be removed in HA 2025.12
|
||||||
host_connnection = (CONNECTION_NETWORK_MAC, host.api.mac_address)
|
host_connnection = (CONNECTION_NETWORK_MAC, host.api.mac_address)
|
||||||
if host_connnection in device.connections:
|
if host_connnection in device.connections:
|
||||||
new_connections = device.connections.copy()
|
new_connections = device.connections.copy()
|
||||||
new_connections.remove(host_connnection)
|
new_connections.remove(host_connnection)
|
||||||
device_reg.async_update_device(device.id, new_connections=new_connections)
|
_LOGGER.debug(
|
||||||
|
"Updating Reolink device connections from %s to %s",
|
||||||
|
device.connections,
|
||||||
|
new_connections,
|
||||||
|
)
|
||||||
|
device_reg.async_update_device(
|
||||||
|
device.id, new_connections=new_connections
|
||||||
|
)
|
||||||
|
|
||||||
ch_device_ids[device.id] = ch
|
ch_device_ids[device.id] = ch
|
||||||
if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid(ch):
|
if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid(
|
||||||
|
ch
|
||||||
|
):
|
||||||
if host.api.supported(None, "UID"):
|
if host.api.supported(None, "UID"):
|
||||||
new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}"
|
new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}"
|
||||||
else:
|
else:
|
||||||
new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}"
|
new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}"
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Updating Reolink device UID from %s to %s", device_uid, new_device_id
|
"Updating Reolink device UID from %s to %s",
|
||||||
|
device_uid,
|
||||||
|
new_device_id,
|
||||||
)
|
)
|
||||||
new_identifiers = {(DOMAIN, new_device_id)}
|
new_identifiers = {(DOMAIN, new_device_id)}
|
||||||
existing_device = device_reg.async_get_device(identifiers=new_identifiers)
|
existing_device = device_reg.async_get_device(
|
||||||
|
identifiers=new_identifiers
|
||||||
|
)
|
||||||
if existing_device is None:
|
if existing_device is None:
|
||||||
device_reg.async_update_device(
|
device_reg.async_update_device(
|
||||||
device.id, new_identifiers=new_identifiers
|
device.id, new_identifiers=new_identifiers
|
||||||
|
@ -198,7 +198,14 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
|||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return super().available and self._host.api.camera_online(self._channel)
|
if self.entity_description.always_available:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return (
|
||||||
|
super().available
|
||||||
|
and self._host.api.camera_online(self._channel)
|
||||||
|
and not self._host.api.baichuan.privacy_mode(self._channel)
|
||||||
|
)
|
||||||
|
|
||||||
def register_callback(self, callback_id: str, cmd_id: int) -> None:
|
def register_callback(self, callback_id: str, cmd_id: int) -> None:
|
||||||
"""Register callback for TCP push events."""
|
"""Register callback for TCP push events."""
|
||||||
|
@ -465,8 +465,9 @@ class ReolinkHost:
|
|||||||
wake = True
|
wake = True
|
||||||
self.last_wake = time()
|
self.last_wake = time()
|
||||||
|
|
||||||
if self._api.baichuan.privacy_mode():
|
for channel in self._api.channels:
|
||||||
await self._api.baichuan.get_privacy_mode()
|
if self._api.baichuan.privacy_mode(channel):
|
||||||
|
await self._api.baichuan.get_privacy_mode(channel)
|
||||||
if self._api.baichuan.privacy_mode():
|
if self._api.baichuan.privacy_mode():
|
||||||
return # API is shutdown, no need to check states
|
return # API is shutdown, no need to check states
|
||||||
|
|
||||||
@ -580,7 +581,12 @@ class ReolinkHost:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
await self._api.subscribe(self._webhook_url)
|
await self._api.subscribe(self._webhook_url)
|
||||||
|
except NotSupportedError as err:
|
||||||
|
self._onvif_push_supported = False
|
||||||
|
_LOGGER.debug(err)
|
||||||
|
return
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Host %s: subscribed successfully to webhook %s",
|
"Host %s: subscribed successfully to webhook %s",
|
||||||
@ -601,7 +607,11 @@ class ReolinkHost:
|
|||||||
return # API is shutdown, no need to subscribe
|
return # API is shutdown, no need to subscribe
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self._onvif_push_supported and not self._api.baichuan.events_active:
|
if (
|
||||||
|
self._onvif_push_supported
|
||||||
|
and not self._api.baichuan.events_active
|
||||||
|
and self._cancel_tcp_push_check is None
|
||||||
|
):
|
||||||
await self._renew(SubType.push)
|
await self._renew(SubType.push)
|
||||||
|
|
||||||
if self._onvif_long_poll_supported and self._long_poll_task is not None:
|
if self._onvif_long_poll_supported and self._long_poll_task is not None:
|
||||||
|
@ -19,5 +19,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["reolink_aio"],
|
"loggers": ["reolink_aio"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["reolink-aio==0.13.2"]
|
"requirements": ["reolink-aio==0.13.3"]
|
||||||
}
|
}
|
||||||
|
@ -76,13 +76,18 @@ def get_store(hass: HomeAssistant, config_entry_id: str) -> Store[str]:
|
|||||||
|
|
||||||
|
|
||||||
def get_device_uid_and_ch(
|
def get_device_uid_and_ch(
|
||||||
device: dr.DeviceEntry, host: ReolinkHost
|
device: dr.DeviceEntry | tuple[str, str], host: ReolinkHost
|
||||||
) -> tuple[list[str], int | None, bool]:
|
) -> tuple[list[str], int | None, bool]:
|
||||||
"""Get the channel and the split device_uid from a reolink DeviceEntry."""
|
"""Get the channel and the split device_uid from a reolink DeviceEntry."""
|
||||||
device_uid = []
|
device_uid = []
|
||||||
is_chime = False
|
is_chime = False
|
||||||
|
|
||||||
for dev_id in device.identifiers:
|
if isinstance(device, dr.DeviceEntry):
|
||||||
|
dev_ids = device.identifiers
|
||||||
|
else:
|
||||||
|
dev_ids = {device}
|
||||||
|
|
||||||
|
for dev_id in dev_ids:
|
||||||
if dev_id[0] == DOMAIN:
|
if dev_id[0] == DOMAIN:
|
||||||
device_uid = dev_id[1].split("_")
|
device_uid = dev_id[1].split("_")
|
||||||
if device_uid[0] == host.unique_id:
|
if device_uid[0] == host.unique_id:
|
||||||
|
@ -28,7 +28,7 @@ from roborock.version_a01_apis import RoborockClientA01
|
|||||||
from roborock.web_api import RoborockApiClient
|
from roborock.web_api import RoborockApiClient
|
||||||
from vacuum_map_parser_base.config.color import ColorsPalette
|
from vacuum_map_parser_base.config.color import ColorsPalette
|
||||||
from vacuum_map_parser_base.config.image_config import ImageConfig
|
from vacuum_map_parser_base.config.image_config import ImageConfig
|
||||||
from vacuum_map_parser_base.config.size import Sizes
|
from vacuum_map_parser_base.config.size import Size, Sizes
|
||||||
from vacuum_map_parser_base.map_data import MapData
|
from vacuum_map_parser_base.map_data import MapData
|
||||||
from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser
|
from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser
|
||||||
|
|
||||||
@ -148,7 +148,13 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
|||||||
]
|
]
|
||||||
self.map_parser = RoborockMapDataParser(
|
self.map_parser = RoborockMapDataParser(
|
||||||
ColorsPalette(),
|
ColorsPalette(),
|
||||||
Sizes({k: v * MAP_SCALE for k, v in Sizes.SIZES.items()}),
|
Sizes(
|
||||||
|
{
|
||||||
|
k: v * MAP_SCALE
|
||||||
|
for k, v in Sizes.SIZES.items()
|
||||||
|
if k != Size.MOP_PATH_WIDTH
|
||||||
|
}
|
||||||
|
),
|
||||||
drawables,
|
drawables,
|
||||||
ImageConfig(scale=MAP_SCALE),
|
ImageConfig(scale=MAP_SCALE),
|
||||||
[],
|
[],
|
||||||
|
@ -252,7 +252,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
|
|||||||
return features
|
return features
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_humidity(self) -> int | None:
|
def current_humidity(self) -> float | None:
|
||||||
"""Return the current humidity."""
|
"""Return the current humidity."""
|
||||||
return self.device_data.humidity
|
return self.device_data.humidity
|
||||||
|
|
||||||
|
@ -15,5 +15,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pysensibo"],
|
"loggers": ["pysensibo"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pysensibo==1.1.0"]
|
"requirements": ["pysensibo==1.2.1"]
|
||||||
}
|
}
|
||||||
|
@ -101,14 +101,25 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = (
|
|||||||
value_fn=lambda data: data.temperature,
|
value_fn=lambda data: data.temperature,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _pure_aqi(pm25_pure: PureAQI | None) -> str | None:
|
||||||
|
"""Return the Pure aqi name or None if unknown."""
|
||||||
|
if pm25_pure:
|
||||||
|
aqi_name = pm25_pure.name.lower()
|
||||||
|
if aqi_name != "unknown":
|
||||||
|
return aqi_name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = (
|
PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = (
|
||||||
SensiboDeviceSensorEntityDescription(
|
SensiboDeviceSensorEntityDescription(
|
||||||
key="pm25",
|
key="pm25",
|
||||||
translation_key="pm25_pure",
|
translation_key="pm25_pure",
|
||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
value_fn=lambda data: data.pm25_pure.name.lower() if data.pm25_pure else None,
|
value_fn=lambda data: _pure_aqi(data.pm25_pure),
|
||||||
extra_fn=None,
|
extra_fn=None,
|
||||||
options=[aqi.name.lower() for aqi in PureAQI],
|
options=[aqi.name.lower() for aqi in PureAQI if aqi.name != "UNKNOWN"],
|
||||||
),
|
),
|
||||||
SensiboDeviceSensorEntityDescription(
|
SensiboDeviceSensorEntityDescription(
|
||||||
key="pure_sensitivity",
|
key="pure_sensitivity",
|
||||||
@ -119,6 +130,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = (
|
|||||||
FILTER_LAST_RESET_DESCRIPTION,
|
FILTER_LAST_RESET_DESCRIPTION,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = (
|
DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = (
|
||||||
SensiboDeviceSensorEntityDescription(
|
SensiboDeviceSensorEntityDescription(
|
||||||
key="timer_time",
|
key="timer_time",
|
||||||
|
@ -13,6 +13,7 @@ from aiohttp import ClientResponseError
|
|||||||
from pysmartthings import (
|
from pysmartthings import (
|
||||||
Attribute,
|
Attribute,
|
||||||
Capability,
|
Capability,
|
||||||
|
Category,
|
||||||
ComponentStatus,
|
ComponentStatus,
|
||||||
Device,
|
Device,
|
||||||
DeviceEvent,
|
DeviceEvent,
|
||||||
@ -32,6 +33,7 @@ from homeassistant.const import (
|
|||||||
ATTR_HW_VERSION,
|
ATTR_HW_VERSION,
|
||||||
ATTR_MANUFACTURER,
|
ATTR_MANUFACTURER,
|
||||||
ATTR_MODEL,
|
ATTR_MODEL,
|
||||||
|
ATTR_SUGGESTED_AREA,
|
||||||
ATTR_SW_VERSION,
|
ATTR_SW_VERSION,
|
||||||
ATTR_VIA_DEVICE,
|
ATTR_VIA_DEVICE,
|
||||||
CONF_ACCESS_TOKEN,
|
CONF_ACCESS_TOKEN,
|
||||||
@ -193,6 +195,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
|
|||||||
}
|
}
|
||||||
devices = await client.get_devices()
|
devices = await client.get_devices()
|
||||||
for device in devices:
|
for device in devices:
|
||||||
|
if (
|
||||||
|
(main_component := device.components.get(MAIN)) is not None
|
||||||
|
and main_component.manufacturer_category is Category.BLUETOOTH_TRACKER
|
||||||
|
):
|
||||||
|
device_status[device.device_id] = FullDevice(
|
||||||
|
device=device,
|
||||||
|
status={},
|
||||||
|
online=True,
|
||||||
|
)
|
||||||
|
continue
|
||||||
status = process_status(await client.get_device_status(device.device_id))
|
status = process_status(await client.get_device_status(device.device_id))
|
||||||
online = await client.get_device_health(device.device_id)
|
online = await client.get_device_health(device.device_id)
|
||||||
device_status[device.device_id] = FullDevice(
|
device_status[device.device_id] = FullDevice(
|
||||||
@ -453,14 +465,24 @@ def create_devices(
|
|||||||
ATTR_SW_VERSION: viper.software_version,
|
ATTR_SW_VERSION: viper.software_version,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
device_registry.async_get_device({(DOMAIN, device.device.device_id)})
|
||||||
|
is None
|
||||||
|
):
|
||||||
|
kwargs.update(
|
||||||
|
{
|
||||||
|
ATTR_SUGGESTED_AREA: (
|
||||||
|
rooms.get(device.device.room_id)
|
||||||
|
if device.device.room_id
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
device_registry.async_get_or_create(
|
device_registry.async_get_or_create(
|
||||||
config_entry_id=entry.entry_id,
|
config_entry_id=entry.entry_id,
|
||||||
identifiers={(DOMAIN, device.device.device_id)},
|
identifiers={(DOMAIN, device.device.device_id)},
|
||||||
configuration_url="https://account.smartthings.com",
|
configuration_url="https://account.smartthings.com",
|
||||||
name=device.device.label,
|
name=device.device.label,
|
||||||
suggested_area=(
|
|
||||||
rooms.get(device.device.room_id) if device.device.room_id else None
|
|
||||||
),
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ from .entity import SmartThingsEntity
|
|||||||
|
|
||||||
ATTR_OPERATION_STATE = "operation_state"
|
ATTR_OPERATION_STATE = "operation_state"
|
||||||
MODE_TO_STATE = {
|
MODE_TO_STATE = {
|
||||||
"auto": HVACMode.HEAT_COOL,
|
"auto": HVACMode.AUTO,
|
||||||
"cool": HVACMode.COOL,
|
"cool": HVACMode.COOL,
|
||||||
"eco": HVACMode.AUTO,
|
"eco": HVACMode.AUTO,
|
||||||
"rush hour": HVACMode.AUTO,
|
"rush hour": HVACMode.AUTO,
|
||||||
@ -40,7 +40,7 @@ MODE_TO_STATE = {
|
|||||||
"off": HVACMode.OFF,
|
"off": HVACMode.OFF,
|
||||||
}
|
}
|
||||||
STATE_TO_MODE = {
|
STATE_TO_MODE = {
|
||||||
HVACMode.HEAT_COOL: "auto",
|
HVACMode.AUTO: "auto",
|
||||||
HVACMode.COOL: "cool",
|
HVACMode.COOL: "cool",
|
||||||
HVACMode.HEAT: "heat",
|
HVACMode.HEAT: "heat",
|
||||||
HVACMode.OFF: "off",
|
HVACMode.OFF: "off",
|
||||||
@ -58,7 +58,7 @@ OPERATING_STATE_TO_ACTION = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AC_MODE_TO_STATE = {
|
AC_MODE_TO_STATE = {
|
||||||
"auto": HVACMode.HEAT_COOL,
|
"auto": HVACMode.AUTO,
|
||||||
"cool": HVACMode.COOL,
|
"cool": HVACMode.COOL,
|
||||||
"dry": HVACMode.DRY,
|
"dry": HVACMode.DRY,
|
||||||
"coolClean": HVACMode.COOL,
|
"coolClean": HVACMode.COOL,
|
||||||
@ -66,10 +66,11 @@ AC_MODE_TO_STATE = {
|
|||||||
"heat": HVACMode.HEAT,
|
"heat": HVACMode.HEAT,
|
||||||
"heatClean": HVACMode.HEAT,
|
"heatClean": HVACMode.HEAT,
|
||||||
"fanOnly": HVACMode.FAN_ONLY,
|
"fanOnly": HVACMode.FAN_ONLY,
|
||||||
|
"fan": HVACMode.FAN_ONLY,
|
||||||
"wind": HVACMode.FAN_ONLY,
|
"wind": HVACMode.FAN_ONLY,
|
||||||
}
|
}
|
||||||
STATE_TO_AC_MODE = {
|
STATE_TO_AC_MODE = {
|
||||||
HVACMode.HEAT_COOL: "auto",
|
HVACMode.AUTO: "auto",
|
||||||
HVACMode.COOL: "cool",
|
HVACMode.COOL: "cool",
|
||||||
HVACMode.DRY: "dry",
|
HVACMode.DRY: "dry",
|
||||||
HVACMode.HEAT: "heat",
|
HVACMode.HEAT: "heat",
|
||||||
@ -88,6 +89,7 @@ FAN_OSCILLATION_TO_SWING = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WIND = "wind"
|
WIND = "wind"
|
||||||
|
FAN = "fan"
|
||||||
WINDFREE = "windFree"
|
WINDFREE = "windFree"
|
||||||
|
|
||||||
UNIT_MAP = {"C": UnitOfTemperature.CELSIUS, "F": UnitOfTemperature.FAHRENHEIT}
|
UNIT_MAP = {"C": UnitOfTemperature.CELSIUS, "F": UnitOfTemperature.FAHRENHEIT}
|
||||||
@ -388,14 +390,15 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
|
|||||||
tasks.append(self.async_turn_on())
|
tasks.append(self.async_turn_on())
|
||||||
|
|
||||||
mode = STATE_TO_AC_MODE[hvac_mode]
|
mode = STATE_TO_AC_MODE[hvac_mode]
|
||||||
# If new hvac_mode is HVAC_MODE_FAN_ONLY and AirConditioner support "wind" mode the AirConditioner new mode has to be "wind"
|
# If new hvac_mode is HVAC_MODE_FAN_ONLY and AirConditioner support "wind" or "fan" mode the AirConditioner
|
||||||
# The conversion make the mode change working
|
# new mode has to be "wind" or "fan"
|
||||||
# The conversion is made only for device that wrongly has capability "wind" instead "fan_only"
|
|
||||||
if hvac_mode == HVACMode.FAN_ONLY:
|
if hvac_mode == HVACMode.FAN_ONLY:
|
||||||
if WIND in self.get_attribute_value(
|
for fan_mode in (WIND, FAN):
|
||||||
|
if fan_mode in self.get_attribute_value(
|
||||||
Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES
|
Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES
|
||||||
):
|
):
|
||||||
mode = WIND
|
mode = fan_mode
|
||||||
|
break
|
||||||
|
|
||||||
tasks.append(
|
tasks.append(
|
||||||
self.execute_device_command(
|
self.execute_device_command(
|
||||||
|
@ -30,5 +30,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pysmartthings"],
|
"loggers": ["pysmartthings"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pysmartthings==3.2.1"]
|
"requirements": ["pysmartthings==3.2.2"]
|
||||||
}
|
}
|
||||||
|
@ -584,7 +584,7 @@ CAPABILITY_TO_SENSORS: dict[
|
|||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
use_temperature_unit=True,
|
use_temperature_unit=True,
|
||||||
# Set the value to None if it is 0 F (-17 C)
|
# Set the value to None if it is 0 F (-17 C)
|
||||||
value_fn=lambda value: None if value in {0, -17} else value,
|
value_fn=lambda value: None if value in {-17, 0, 1} else value,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -53,7 +53,6 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
try:
|
try:
|
||||||
if not await self._async_check_auth_required(user_input):
|
if not await self._async_check_auth_required(user_input):
|
||||||
info = await self.client.get_info()
|
info = await self.client.get_info()
|
||||||
self._host = str(info.device_ip)
|
|
||||||
self._device_name = str(info.hostname)
|
self._device_name = str(info.hostname)
|
||||||
|
|
||||||
if info.model not in Devices:
|
if info.model not in Devices:
|
||||||
@ -79,7 +78,6 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
try:
|
try:
|
||||||
if not await self._async_check_auth_required(user_input):
|
if not await self._async_check_auth_required(user_input):
|
||||||
info = await self.client.get_info()
|
info = await self.client.get_info()
|
||||||
self._host = str(info.device_ip)
|
|
||||||
self._device_name = str(info.hostname)
|
self._device_name = str(info.hostname)
|
||||||
|
|
||||||
if info.model not in Devices:
|
if info.model not in Devices:
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["snoo"],
|
"loggers": ["snoo"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["python-snoo==0.6.5"]
|
"requirements": ["python-snoo==0.6.6"]
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,8 @@
|
|||||||
"power": "Power button pressed",
|
"power": "Power button pressed",
|
||||||
"status_requested": "Status requested",
|
"status_requested": "Status requested",
|
||||||
"sticky_white_noise_updated": "Sleepytime sounds updated",
|
"sticky_white_noise_updated": "Sleepytime sounds updated",
|
||||||
"config_change": "Config changed"
|
"config_change": "Config changed",
|
||||||
|
"restart": "Restart"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ from tesla_fleet_api.teslemetry import EnergySite, Vehicle
|
|||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@ -229,7 +230,7 @@ class TeslemetryWallConnectorEntity(TeslemetryEntity):
|
|||||||
super().__init__(data.live_coordinator, key)
|
super().__init__(data.live_coordinator, key)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _value(self) -> int:
|
def _value(self) -> StateType:
|
||||||
"""Return a specific wall connector value from coordinator data."""
|
"""Return a specific wall connector value from coordinator data."""
|
||||||
return (
|
return (
|
||||||
self.coordinator.data.get("wall_connectors", {})
|
self.coordinator.data.get("wall_connectors", {})
|
||||||
|
@ -1763,7 +1763,6 @@ class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorE
|
|||||||
|
|
||||||
def _async_update_attrs(self) -> None:
|
def _async_update_attrs(self) -> None:
|
||||||
"""Update the attributes of the sensor."""
|
"""Update the attributes of the sensor."""
|
||||||
if self.exists:
|
|
||||||
self._attr_native_value = self.entity_description.value_fn(self._value)
|
self._attr_native_value = self.entity_description.value_fn(self._value)
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["tibber"],
|
"loggers": ["tibber"],
|
||||||
"requirements": ["pyTibber==0.30.8"]
|
"requirements": ["pyTibber==0.31.2"]
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["holidays"],
|
"loggers": ["holidays"],
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["holidays==0.70"]
|
"requirements": ["holidays==0.72"]
|
||||||
}
|
}
|
||||||
|
@ -419,13 +419,26 @@ class ZHADeviceProxy(EventBase):
|
|||||||
@callback
|
@callback
|
||||||
def handle_zha_event(self, zha_event: ZHAEvent) -> None:
|
def handle_zha_event(self, zha_event: ZHAEvent) -> None:
|
||||||
"""Handle a ZHA event."""
|
"""Handle a ZHA event."""
|
||||||
|
if ATTR_UNIQUE_ID in zha_event.data:
|
||||||
|
unique_id = zha_event.data[ATTR_UNIQUE_ID]
|
||||||
|
|
||||||
|
# Client cluster handler unique IDs in the ZHA lib were disambiguated by
|
||||||
|
# adding a suffix of `_CLIENT`. Unfortunately, this breaks existing
|
||||||
|
# automations that match the `unique_id` key. This can be removed in a
|
||||||
|
# future release with proper notice of a breaking change.
|
||||||
|
unique_id = unique_id.removesuffix("_CLIENT")
|
||||||
|
else:
|
||||||
|
unique_id = zha_event.unique_id
|
||||||
|
|
||||||
self.gateway_proxy.hass.bus.async_fire(
|
self.gateway_proxy.hass.bus.async_fire(
|
||||||
ZHA_EVENT,
|
ZHA_EVENT,
|
||||||
{
|
{
|
||||||
ATTR_DEVICE_IEEE: str(zha_event.device_ieee),
|
ATTR_DEVICE_IEEE: str(zha_event.device_ieee),
|
||||||
ATTR_UNIQUE_ID: zha_event.unique_id,
|
|
||||||
ATTR_DEVICE_ID: self.device_id,
|
ATTR_DEVICE_ID: self.device_id,
|
||||||
**zha_event.data,
|
**zha_event.data,
|
||||||
|
# The order of these keys is intentional, `zha_event.data` can contain
|
||||||
|
# a `unique_id` key, which we explicitly replace
|
||||||
|
ATTR_UNIQUE_ID: unique_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -278,6 +278,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
# and we'll handle the clean up below.
|
# and we'll handle the clean up below.
|
||||||
await driver_events.setup(driver)
|
await driver_events.setup(driver)
|
||||||
|
|
||||||
|
if (old_unique_id := entry.unique_id) is not None and old_unique_id != (
|
||||||
|
new_unique_id := str(driver.controller.home_id)
|
||||||
|
):
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
controller_model = "Unknown model"
|
||||||
|
if (
|
||||||
|
(own_node := driver.controller.own_node)
|
||||||
|
and (
|
||||||
|
controller_device_entry := device_registry.async_get_device(
|
||||||
|
identifiers={get_device_id(driver, own_node)}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
and (model := controller_device_entry.model)
|
||||||
|
):
|
||||||
|
controller_model = model
|
||||||
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
f"migrate_unique_id.{entry.entry_id}",
|
||||||
|
data={
|
||||||
|
"config_entry_id": entry.entry_id,
|
||||||
|
"config_entry_title": entry.title,
|
||||||
|
"controller_model": controller_model,
|
||||||
|
"new_unique_id": new_unique_id,
|
||||||
|
"old_unique_id": old_unique_id,
|
||||||
|
},
|
||||||
|
is_fixable=True,
|
||||||
|
severity=IssueSeverity.ERROR,
|
||||||
|
translation_key="migrate_unique_id",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
async_delete_issue(hass, DOMAIN, f"migrate_unique_id.{entry.entry_id}")
|
||||||
|
|
||||||
# If the listen task is already failed, we need to raise ConfigEntryNotReady
|
# If the listen task is already failed, we need to raise ConfigEntryNotReady
|
||||||
if listen_task.done():
|
if listen_task.done():
|
||||||
listen_error, error_message = _get_listen_task_error(listen_task)
|
listen_error, error_message = _get_listen_task_error(listen_task)
|
||||||
|
@ -71,6 +71,7 @@ from homeassistant.components.websocket_api import (
|
|||||||
ActiveConnection,
|
ActiveConnection,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
|
from homeassistant.const import CONF_URL
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
@ -88,13 +89,16 @@ from .const import (
|
|||||||
DATA_CLIENT,
|
DATA_CLIENT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
||||||
|
LOGGER,
|
||||||
RESTORE_NVM_DRIVER_READY_TIMEOUT,
|
RESTORE_NVM_DRIVER_READY_TIMEOUT,
|
||||||
USER_AGENT,
|
USER_AGENT,
|
||||||
)
|
)
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
|
CannotConnect,
|
||||||
async_enable_statistics,
|
async_enable_statistics,
|
||||||
async_get_node_from_device_id,
|
async_get_node_from_device_id,
|
||||||
async_get_provisioning_entry_from_device_id,
|
async_get_provisioning_entry_from_device_id,
|
||||||
|
async_get_version_info,
|
||||||
get_device_id,
|
get_device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2857,6 +2861,25 @@ async def websocket_hard_reset_controller(
|
|||||||
async with asyncio.timeout(HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT):
|
async with asyncio.timeout(HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT):
|
||||||
await wait_driver_ready.wait()
|
await wait_driver_ready.wait()
|
||||||
|
|
||||||
|
# When resetting the controller, the controller home id is also changed.
|
||||||
|
# The controller state in the client is stale after resetting the controller,
|
||||||
|
# so get the new home id with a new client using the helper function.
|
||||||
|
# The client state will be refreshed by reloading the config entry,
|
||||||
|
# after the unique id of the config entry has been updated.
|
||||||
|
try:
|
||||||
|
version_info = await async_get_version_info(hass, entry.data[CONF_URL])
|
||||||
|
except CannotConnect:
|
||||||
|
# Just log this error, as there's nothing to do about it here.
|
||||||
|
# The stale unique id needs to be handled by a repair flow,
|
||||||
|
# after the config entry has been reloaded.
|
||||||
|
LOGGER.error(
|
||||||
|
"Failed to get server version, cannot update config entry"
|
||||||
|
"unique id with new home id, after controller reset"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry, unique_id=str(version_info.home_id)
|
||||||
|
)
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,14 +9,13 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
from serial.tools import list_ports
|
from serial.tools import list_ports
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from zwave_js_server.client import Client
|
from zwave_js_server.client import Client
|
||||||
from zwave_js_server.exceptions import FailedCommand
|
from zwave_js_server.exceptions import FailedCommand
|
||||||
from zwave_js_server.model.driver import Driver
|
from zwave_js_server.model.driver import Driver
|
||||||
from zwave_js_server.version import VersionInfo, get_server_version
|
from zwave_js_server.version import VersionInfo
|
||||||
|
|
||||||
from homeassistant.components import usb
|
from homeassistant.components import usb
|
||||||
from homeassistant.components.hassio import (
|
from homeassistant.components.hassio import (
|
||||||
@ -36,7 +35,6 @@ from homeassistant.const import CONF_NAME, CONF_URL
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.data_entry_flow import AbortFlow
|
from homeassistant.data_entry_flow import AbortFlow
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.helpers.hassio import is_hassio
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||||
from homeassistant.helpers.service_info.usb import UsbServiceInfo
|
from homeassistant.helpers.service_info.usb import UsbServiceInfo
|
||||||
@ -69,6 +67,7 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
RESTORE_NVM_DRIVER_READY_TIMEOUT,
|
RESTORE_NVM_DRIVER_READY_TIMEOUT,
|
||||||
)
|
)
|
||||||
|
from .helpers import CannotConnect, async_get_version_info
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -79,7 +78,6 @@ ADDON_SETUP_TIMEOUT = 5
|
|||||||
ADDON_SETUP_TIMEOUT_ROUNDS = 40
|
ADDON_SETUP_TIMEOUT_ROUNDS = 40
|
||||||
CONF_EMULATE_HARDWARE = "emulate_hardware"
|
CONF_EMULATE_HARDWARE = "emulate_hardware"
|
||||||
CONF_LOG_LEVEL = "log_level"
|
CONF_LOG_LEVEL = "log_level"
|
||||||
SERVER_VERSION_TIMEOUT = 10
|
|
||||||
|
|
||||||
ADDON_LOG_LEVELS = {
|
ADDON_LOG_LEVELS = {
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
@ -130,22 +128,6 @@ async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo:
|
|||||||
raise InvalidInput("cannot_connect") from err
|
raise InvalidInput("cannot_connect") from err
|
||||||
|
|
||||||
|
|
||||||
async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> VersionInfo:
|
|
||||||
"""Return Z-Wave JS version info."""
|
|
||||||
try:
|
|
||||||
async with asyncio.timeout(SERVER_VERSION_TIMEOUT):
|
|
||||||
version_info: VersionInfo = await get_server_version(
|
|
||||||
ws_address, async_get_clientsession(hass)
|
|
||||||
)
|
|
||||||
except (TimeoutError, aiohttp.ClientError) as err:
|
|
||||||
# We don't want to spam the log if the add-on isn't started
|
|
||||||
# or takes a long time to start.
|
|
||||||
_LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err)
|
|
||||||
raise CannotConnect from err
|
|
||||||
|
|
||||||
return version_info
|
|
||||||
|
|
||||||
|
|
||||||
def get_usb_ports() -> dict[str, str]:
|
def get_usb_ports() -> dict[str, str]:
|
||||||
"""Return a dict of USB ports and their friendly names."""
|
"""Return a dict of USB ports and their friendly names."""
|
||||||
ports = list_ports.comports()
|
ports = list_ports.comports()
|
||||||
@ -1357,10 +1339,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
return client.driver
|
return client.driver
|
||||||
|
|
||||||
|
|
||||||
class CannotConnect(HomeAssistantError):
|
|
||||||
"""Indicate connection error."""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidInput(HomeAssistantError):
|
class InvalidInput(HomeAssistantError):
|
||||||
"""Error to indicate input data is invalid."""
|
"""Error to indicate input data is invalid."""
|
||||||
|
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import astuple, dataclass
|
from dataclasses import astuple, dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from zwave_js_server.client import Client as ZwaveClient
|
from zwave_js_server.client import Client as ZwaveClient
|
||||||
from zwave_js_server.const import (
|
from zwave_js_server.const import (
|
||||||
@ -25,6 +27,7 @@ from zwave_js_server.model.value import (
|
|||||||
ValueDataType,
|
ValueDataType,
|
||||||
get_value_id_str,
|
get_value_id_str,
|
||||||
)
|
)
|
||||||
|
from zwave_js_server.version import VersionInfo, get_server_version
|
||||||
|
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
@ -38,6 +41,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.group import expand_entity_ids
|
from homeassistant.helpers.group import expand_entity_ids
|
||||||
from homeassistant.helpers.typing import ConfigType, VolSchemaType
|
from homeassistant.helpers.typing import ConfigType, VolSchemaType
|
||||||
@ -54,6 +58,8 @@ from .const import (
|
|||||||
LOGGER,
|
LOGGER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SERVER_VERSION_TIMEOUT = 10
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ZwaveValueID:
|
class ZwaveValueID:
|
||||||
@ -568,3 +574,23 @@ def get_network_identifier_for_notification(
|
|||||||
return f"`{config_entry.title}`, with the home ID `{home_id}`,"
|
return f"`{config_entry.title}`, with the home ID `{home_id}`,"
|
||||||
return f"with the home ID `{home_id}`"
|
return f"with the home ID `{home_id}`"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> VersionInfo:
|
||||||
|
"""Return Z-Wave JS version info."""
|
||||||
|
try:
|
||||||
|
async with asyncio.timeout(SERVER_VERSION_TIMEOUT):
|
||||||
|
version_info: VersionInfo = await get_server_version(
|
||||||
|
ws_address, async_get_clientsession(hass)
|
||||||
|
)
|
||||||
|
except (TimeoutError, aiohttp.ClientError) as err:
|
||||||
|
# We don't want to spam the log if the add-on isn't started
|
||||||
|
# or takes a long time to start.
|
||||||
|
LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err)
|
||||||
|
raise CannotConnect from err
|
||||||
|
|
||||||
|
return version_info
|
||||||
|
|
||||||
|
|
||||||
|
class CannotConnect(HomeAssistantError):
|
||||||
|
"""Indicate connection error."""
|
||||||
|
@ -57,6 +57,47 @@ class DeviceConfigFileChangedFlow(RepairsFlow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MigrateUniqueIDFlow(RepairsFlow):
|
||||||
|
"""Handler for an issue fixing flow."""
|
||||||
|
|
||||||
|
def __init__(self, data: dict[str, str]) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
self.description_placeholders: dict[str, str] = {
|
||||||
|
"config_entry_title": data["config_entry_title"],
|
||||||
|
"controller_model": data["controller_model"],
|
||||||
|
"new_unique_id": data["new_unique_id"],
|
||||||
|
"old_unique_id": data["old_unique_id"],
|
||||||
|
}
|
||||||
|
self._config_entry_id: str = data["config_entry_id"]
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: dict[str, str] | None = None
|
||||||
|
) -> data_entry_flow.FlowResult:
|
||||||
|
"""Handle the first step of a fix flow."""
|
||||||
|
return await self.async_step_confirm()
|
||||||
|
|
||||||
|
async def async_step_confirm(
|
||||||
|
self, user_input: dict[str, str] | None = None
|
||||||
|
) -> data_entry_flow.FlowResult:
|
||||||
|
"""Handle the confirm step of a fix flow."""
|
||||||
|
if user_input is not None:
|
||||||
|
config_entry = self.hass.config_entries.async_get_entry(
|
||||||
|
self._config_entry_id
|
||||||
|
)
|
||||||
|
# If config entry was removed, we can ignore the issue.
|
||||||
|
if config_entry is not None:
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
config_entry,
|
||||||
|
unique_id=self.description_placeholders["new_unique_id"],
|
||||||
|
)
|
||||||
|
return self.async_create_entry(data={})
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="confirm",
|
||||||
|
description_placeholders=self.description_placeholders,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_create_fix_flow(
|
async def async_create_fix_flow(
|
||||||
hass: HomeAssistant, issue_id: str, data: dict[str, str] | None
|
hass: HomeAssistant, issue_id: str, data: dict[str, str] | None
|
||||||
) -> RepairsFlow:
|
) -> RepairsFlow:
|
||||||
@ -65,4 +106,7 @@ async def async_create_fix_flow(
|
|||||||
if issue_id.split(".")[0] == "device_config_file_changed":
|
if issue_id.split(".")[0] == "device_config_file_changed":
|
||||||
assert data
|
assert data
|
||||||
return DeviceConfigFileChangedFlow(data)
|
return DeviceConfigFileChangedFlow(data)
|
||||||
|
if issue_id.split(".")[0] == "migrate_unique_id":
|
||||||
|
assert data
|
||||||
|
return MigrateUniqueIDFlow(data)
|
||||||
return ConfirmRepairFlow()
|
return ConfirmRepairFlow()
|
||||||
|
@ -273,6 +273,17 @@
|
|||||||
"invalid_server_version": {
|
"invalid_server_version": {
|
||||||
"description": "The version of Z-Wave Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave Server to the latest version to fix this issue.",
|
"description": "The version of Z-Wave Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave Server to the latest version to fix this issue.",
|
||||||
"title": "Newer version of Z-Wave Server needed"
|
"title": "Newer version of Z-Wave Server needed"
|
||||||
|
},
|
||||||
|
"migrate_unique_id": {
|
||||||
|
"fix_flow": {
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"description": "A Z-Wave controller of model {controller_model} with a different ID ({new_unique_id}) than the previously connected controller ({old_unique_id}) was connected to the {config_entry_title} configuration entry.\n\nReasons for a different controller ID could be:\n\n1. The controller was factory reset, with a 3rd party application.\n2. A controller Non Volatile Memory (NVM) backup was restored to the controller, with a 3rd party application.\n3. A different controller was connected to this configuration entry.\n\nIf a different controller was connected, you should instead set up a new configuration entry for the new controller.\n\nIf you are sure that the current controller is the correct controller you can confirm this by pressing Submit, and the configuration entry will remember the new controller ID.",
|
||||||
|
"title": "An unknown controller was detected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "An unknown controller was detected"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
|||||||
APPLICATION_NAME: Final = "HomeAssistant"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2025
|
MAJOR_VERSION: Final = 2025
|
||||||
MINOR_VERSION: Final = 5
|
MINOR_VERSION: Final = 5
|
||||||
PATCH_VERSION: Final = "1"
|
PATCH_VERSION: Final = "2"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Automatically generated by gen_requirements_all.py, do not edit
|
# Automatically generated by gen_requirements_all.py, do not edit
|
||||||
|
|
||||||
aiodhcpwatcher==1.1.1
|
aiodhcpwatcher==1.1.1
|
||||||
aiodiscover==2.6.1
|
aiodiscover==2.7.0
|
||||||
aiodns==3.4.0
|
aiodns==3.4.0
|
||||||
aiohasupervisor==0.3.1
|
aiohasupervisor==0.3.1
|
||||||
aiohttp-asyncmdnsresolver==0.1.1
|
aiohttp-asyncmdnsresolver==0.1.1
|
||||||
@ -38,7 +38,7 @@ habluetooth==3.48.2
|
|||||||
hass-nabucasa==0.96.0
|
hass-nabucasa==0.96.0
|
||||||
hassil==2.2.3
|
hassil==2.2.3
|
||||||
home-assistant-bluetooth==1.13.1
|
home-assistant-bluetooth==1.13.1
|
||||||
home-assistant-frontend==20250509.0
|
home-assistant-frontend==20250516.0
|
||||||
home-assistant-intents==2025.5.7
|
home-assistant-intents==2025.5.7
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
@ -70,7 +70,7 @@ typing-extensions>=4.13.0,<5.0
|
|||||||
ulid-transform==1.4.0
|
ulid-transform==1.4.0
|
||||||
urllib3>=1.26.5,<2
|
urllib3>=1.26.5,<2
|
||||||
uv==0.7.1
|
uv==0.7.1
|
||||||
voluptuous-openapi==0.0.7
|
voluptuous-openapi==0.1.0
|
||||||
voluptuous-serialize==2.6.0
|
voluptuous-serialize==2.6.0
|
||||||
voluptuous==0.15.2
|
voluptuous==0.15.2
|
||||||
webrtc-models==0.3.0
|
webrtc-models==0.3.0
|
||||||
@ -217,3 +217,8 @@ aiofiles>=24.1.0
|
|||||||
# https://github.com/aio-libs/multidict/issues/1134
|
# https://github.com/aio-libs/multidict/issues/1134
|
||||||
# https://github.com/aio-libs/multidict/issues/1131
|
# https://github.com/aio-libs/multidict/issues/1131
|
||||||
multidict>=6.4.2
|
multidict>=6.4.2
|
||||||
|
|
||||||
|
# rpds-py > 0.25.0 requires cargo 1.84.0
|
||||||
|
# Stable Alpine current only ships cargo 1.83.0
|
||||||
|
# No wheels upstream available for armhf & armv7
|
||||||
|
rpds-py==0.24.0
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2025.5.1"
|
version = "2025.5.2"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
@ -120,7 +120,7 @@ dependencies = [
|
|||||||
"uv==0.7.1",
|
"uv==0.7.1",
|
||||||
"voluptuous==0.15.2",
|
"voluptuous==0.15.2",
|
||||||
"voluptuous-serialize==2.6.0",
|
"voluptuous-serialize==2.6.0",
|
||||||
"voluptuous-openapi==0.0.7",
|
"voluptuous-openapi==0.1.0",
|
||||||
"yarl==1.20.0",
|
"yarl==1.20.0",
|
||||||
"webrtc-models==0.3.0",
|
"webrtc-models==0.3.0",
|
||||||
"zeroconf==0.147.0",
|
"zeroconf==0.147.0",
|
||||||
|
2
requirements.txt
generated
2
requirements.txt
generated
@ -57,7 +57,7 @@ urllib3>=1.26.5,<2
|
|||||||
uv==0.7.1
|
uv==0.7.1
|
||||||
voluptuous==0.15.2
|
voluptuous==0.15.2
|
||||||
voluptuous-serialize==2.6.0
|
voluptuous-serialize==2.6.0
|
||||||
voluptuous-openapi==0.0.7
|
voluptuous-openapi==0.1.0
|
||||||
yarl==1.20.0
|
yarl==1.20.0
|
||||||
webrtc-models==0.3.0
|
webrtc-models==0.3.0
|
||||||
zeroconf==0.147.0
|
zeroconf==0.147.0
|
||||||
|
32
requirements_all.txt
generated
32
requirements_all.txt
generated
@ -214,13 +214,13 @@ aiobafi6==0.9.0
|
|||||||
aiobotocore==2.21.1
|
aiobotocore==2.21.1
|
||||||
|
|
||||||
# homeassistant.components.comelit
|
# homeassistant.components.comelit
|
||||||
aiocomelit==0.12.0
|
aiocomelit==0.12.1
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
# homeassistant.components.dhcp
|
||||||
aiodhcpwatcher==1.1.1
|
aiodhcpwatcher==1.1.1
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
# homeassistant.components.dhcp
|
||||||
aiodiscover==2.6.1
|
aiodiscover==2.7.0
|
||||||
|
|
||||||
# homeassistant.components.dnsip
|
# homeassistant.components.dnsip
|
||||||
aiodns==3.4.0
|
aiodns==3.4.0
|
||||||
@ -762,7 +762,7 @@ debugpy==1.8.13
|
|||||||
# decora==0.6
|
# decora==0.6
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
deebot-client==13.1.0
|
deebot-client==13.2.0
|
||||||
|
|
||||||
# homeassistant.components.ihc
|
# homeassistant.components.ihc
|
||||||
# homeassistant.components.namecheapdns
|
# homeassistant.components.namecheapdns
|
||||||
@ -986,7 +986,7 @@ gardena-bluetooth==1.6.0
|
|||||||
gassist-text==0.0.12
|
gassist-text==0.0.12
|
||||||
|
|
||||||
# homeassistant.components.google
|
# homeassistant.components.google
|
||||||
gcal-sync==7.0.0
|
gcal-sync==7.0.1
|
||||||
|
|
||||||
# homeassistant.components.geniushub
|
# homeassistant.components.geniushub
|
||||||
geniushub-client==0.7.1
|
geniushub-client==0.7.1
|
||||||
@ -1158,10 +1158,10 @@ hole==0.8.0
|
|||||||
|
|
||||||
# homeassistant.components.holiday
|
# homeassistant.components.holiday
|
||||||
# homeassistant.components.workday
|
# homeassistant.components.workday
|
||||||
holidays==0.70
|
holidays==0.72
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20250509.0
|
home-assistant-frontend==20250516.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2025.5.7
|
home-assistant-intents==2025.5.7
|
||||||
@ -1200,7 +1200,7 @@ ibmiotf==0.3.4
|
|||||||
# homeassistant.components.local_calendar
|
# homeassistant.components.local_calendar
|
||||||
# homeassistant.components.local_todo
|
# homeassistant.components.local_todo
|
||||||
# homeassistant.components.remote_calendar
|
# homeassistant.components.remote_calendar
|
||||||
ical==9.2.0
|
ical==9.2.4
|
||||||
|
|
||||||
# homeassistant.components.caldav
|
# homeassistant.components.caldav
|
||||||
icalendar==6.1.0
|
icalendar==6.1.0
|
||||||
@ -1427,7 +1427,7 @@ microBeesPy==0.3.5
|
|||||||
mill-local==0.3.0
|
mill-local==0.3.0
|
||||||
|
|
||||||
# homeassistant.components.mill
|
# homeassistant.components.mill
|
||||||
millheater==0.12.3
|
millheater==0.12.5
|
||||||
|
|
||||||
# homeassistant.components.minio
|
# homeassistant.components.minio
|
||||||
minio==7.1.12
|
minio==7.1.12
|
||||||
@ -1804,7 +1804,7 @@ pyRFXtrx==0.31.1
|
|||||||
pySDCP==1
|
pySDCP==1
|
||||||
|
|
||||||
# homeassistant.components.tibber
|
# homeassistant.components.tibber
|
||||||
pyTibber==0.30.8
|
pyTibber==0.31.2
|
||||||
|
|
||||||
# homeassistant.components.dlink
|
# homeassistant.components.dlink
|
||||||
pyW215==0.7.0
|
pyW215==0.7.0
|
||||||
@ -1955,7 +1955,7 @@ pyeiscp==0.0.7
|
|||||||
pyemoncms==0.1.1
|
pyemoncms==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.enphase_envoy
|
# homeassistant.components.enphase_envoy
|
||||||
pyenphase==1.26.0
|
pyenphase==1.26.1
|
||||||
|
|
||||||
# homeassistant.components.envisalink
|
# homeassistant.components.envisalink
|
||||||
pyenvisalink==4.7
|
pyenvisalink==4.7
|
||||||
@ -2093,7 +2093,7 @@ pykwb==0.0.8
|
|||||||
pylacrosse==0.4
|
pylacrosse==0.4
|
||||||
|
|
||||||
# homeassistant.components.lamarzocco
|
# homeassistant.components.lamarzocco
|
||||||
pylamarzocco==2.0.1
|
pylamarzocco==2.0.3
|
||||||
|
|
||||||
# homeassistant.components.lastfm
|
# homeassistant.components.lastfm
|
||||||
pylast==5.1.0
|
pylast==5.1.0
|
||||||
@ -2293,7 +2293,7 @@ pysaj==0.0.16
|
|||||||
pyschlage==2025.4.0
|
pyschlage==2025.4.0
|
||||||
|
|
||||||
# homeassistant.components.sensibo
|
# homeassistant.components.sensibo
|
||||||
pysensibo==1.1.0
|
pysensibo==1.2.1
|
||||||
|
|
||||||
# homeassistant.components.serial
|
# homeassistant.components.serial
|
||||||
pyserial-asyncio-fast==0.16
|
pyserial-asyncio-fast==0.16
|
||||||
@ -2326,7 +2326,7 @@ pysma==0.7.5
|
|||||||
pysmappee==0.2.29
|
pysmappee==0.2.29
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartthings==3.2.1
|
pysmartthings==3.2.2
|
||||||
|
|
||||||
# homeassistant.components.smarty
|
# homeassistant.components.smarty
|
||||||
pysmarty2==0.10.2
|
pysmarty2==0.10.2
|
||||||
@ -2437,7 +2437,7 @@ python-juicenet==1.1.0
|
|||||||
python-kasa[speedups]==0.10.2
|
python-kasa[speedups]==0.10.2
|
||||||
|
|
||||||
# homeassistant.components.linkplay
|
# homeassistant.components.linkplay
|
||||||
python-linkplay==0.2.4
|
python-linkplay==0.2.5
|
||||||
|
|
||||||
# homeassistant.components.lirc
|
# homeassistant.components.lirc
|
||||||
# python-lirc==1.2.3
|
# python-lirc==1.2.3
|
||||||
@ -2486,7 +2486,7 @@ python-roborock==2.18.2
|
|||||||
python-smarttub==0.0.39
|
python-smarttub==0.0.39
|
||||||
|
|
||||||
# homeassistant.components.snoo
|
# homeassistant.components.snoo
|
||||||
python-snoo==0.6.5
|
python-snoo==0.6.6
|
||||||
|
|
||||||
# homeassistant.components.songpal
|
# homeassistant.components.songpal
|
||||||
python-songpal==0.16.2
|
python-songpal==0.16.2
|
||||||
@ -2637,7 +2637,7 @@ renault-api==0.3.1
|
|||||||
renson-endura-delta==1.7.2
|
renson-endura-delta==1.7.2
|
||||||
|
|
||||||
# homeassistant.components.reolink
|
# homeassistant.components.reolink
|
||||||
reolink-aio==0.13.2
|
reolink-aio==0.13.3
|
||||||
|
|
||||||
# homeassistant.components.idteck_prox
|
# homeassistant.components.idteck_prox
|
||||||
rfk101py==0.0.1
|
rfk101py==0.0.1
|
||||||
|
32
requirements_test_all.txt
generated
32
requirements_test_all.txt
generated
@ -202,13 +202,13 @@ aiobafi6==0.9.0
|
|||||||
aiobotocore==2.21.1
|
aiobotocore==2.21.1
|
||||||
|
|
||||||
# homeassistant.components.comelit
|
# homeassistant.components.comelit
|
||||||
aiocomelit==0.12.0
|
aiocomelit==0.12.1
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
# homeassistant.components.dhcp
|
||||||
aiodhcpwatcher==1.1.1
|
aiodhcpwatcher==1.1.1
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
# homeassistant.components.dhcp
|
||||||
aiodiscover==2.6.1
|
aiodiscover==2.7.0
|
||||||
|
|
||||||
# homeassistant.components.dnsip
|
# homeassistant.components.dnsip
|
||||||
aiodns==3.4.0
|
aiodns==3.4.0
|
||||||
@ -653,7 +653,7 @@ dbus-fast==2.43.0
|
|||||||
debugpy==1.8.13
|
debugpy==1.8.13
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
deebot-client==13.1.0
|
deebot-client==13.2.0
|
||||||
|
|
||||||
# homeassistant.components.ihc
|
# homeassistant.components.ihc
|
||||||
# homeassistant.components.namecheapdns
|
# homeassistant.components.namecheapdns
|
||||||
@ -840,7 +840,7 @@ gardena-bluetooth==1.6.0
|
|||||||
gassist-text==0.0.12
|
gassist-text==0.0.12
|
||||||
|
|
||||||
# homeassistant.components.google
|
# homeassistant.components.google
|
||||||
gcal-sync==7.0.0
|
gcal-sync==7.0.1
|
||||||
|
|
||||||
# homeassistant.components.geniushub
|
# homeassistant.components.geniushub
|
||||||
geniushub-client==0.7.1
|
geniushub-client==0.7.1
|
||||||
@ -988,10 +988,10 @@ hole==0.8.0
|
|||||||
|
|
||||||
# homeassistant.components.holiday
|
# homeassistant.components.holiday
|
||||||
# homeassistant.components.workday
|
# homeassistant.components.workday
|
||||||
holidays==0.70
|
holidays==0.72
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20250509.0
|
home-assistant-frontend==20250516.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2025.5.7
|
home-assistant-intents==2025.5.7
|
||||||
@ -1021,7 +1021,7 @@ ibeacon-ble==1.2.0
|
|||||||
# homeassistant.components.local_calendar
|
# homeassistant.components.local_calendar
|
||||||
# homeassistant.components.local_todo
|
# homeassistant.components.local_todo
|
||||||
# homeassistant.components.remote_calendar
|
# homeassistant.components.remote_calendar
|
||||||
ical==9.2.0
|
ical==9.2.4
|
||||||
|
|
||||||
# homeassistant.components.caldav
|
# homeassistant.components.caldav
|
||||||
icalendar==6.1.0
|
icalendar==6.1.0
|
||||||
@ -1200,7 +1200,7 @@ microBeesPy==0.3.5
|
|||||||
mill-local==0.3.0
|
mill-local==0.3.0
|
||||||
|
|
||||||
# homeassistant.components.mill
|
# homeassistant.components.mill
|
||||||
millheater==0.12.3
|
millheater==0.12.5
|
||||||
|
|
||||||
# homeassistant.components.minio
|
# homeassistant.components.minio
|
||||||
minio==7.1.12
|
minio==7.1.12
|
||||||
@ -1491,7 +1491,7 @@ pyHomee==1.2.8
|
|||||||
pyRFXtrx==0.31.1
|
pyRFXtrx==0.31.1
|
||||||
|
|
||||||
# homeassistant.components.tibber
|
# homeassistant.components.tibber
|
||||||
pyTibber==0.30.8
|
pyTibber==0.31.2
|
||||||
|
|
||||||
# homeassistant.components.dlink
|
# homeassistant.components.dlink
|
||||||
pyW215==0.7.0
|
pyW215==0.7.0
|
||||||
@ -1600,7 +1600,7 @@ pyeiscp==0.0.7
|
|||||||
pyemoncms==0.1.1
|
pyemoncms==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.enphase_envoy
|
# homeassistant.components.enphase_envoy
|
||||||
pyenphase==1.26.0
|
pyenphase==1.26.1
|
||||||
|
|
||||||
# homeassistant.components.everlights
|
# homeassistant.components.everlights
|
||||||
pyeverlights==0.1.0
|
pyeverlights==0.1.0
|
||||||
@ -1708,7 +1708,7 @@ pykrakenapi==0.1.8
|
|||||||
pykulersky==0.5.8
|
pykulersky==0.5.8
|
||||||
|
|
||||||
# homeassistant.components.lamarzocco
|
# homeassistant.components.lamarzocco
|
||||||
pylamarzocco==2.0.1
|
pylamarzocco==2.0.3
|
||||||
|
|
||||||
# homeassistant.components.lastfm
|
# homeassistant.components.lastfm
|
||||||
pylast==5.1.0
|
pylast==5.1.0
|
||||||
@ -1875,7 +1875,7 @@ pysabnzbd==1.1.1
|
|||||||
pyschlage==2025.4.0
|
pyschlage==2025.4.0
|
||||||
|
|
||||||
# homeassistant.components.sensibo
|
# homeassistant.components.sensibo
|
||||||
pysensibo==1.1.0
|
pysensibo==1.2.1
|
||||||
|
|
||||||
# homeassistant.components.acer_projector
|
# homeassistant.components.acer_projector
|
||||||
# homeassistant.components.crownstone
|
# homeassistant.components.crownstone
|
||||||
@ -1899,7 +1899,7 @@ pysma==0.7.5
|
|||||||
pysmappee==0.2.29
|
pysmappee==0.2.29
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartthings==3.2.1
|
pysmartthings==3.2.2
|
||||||
|
|
||||||
# homeassistant.components.smarty
|
# homeassistant.components.smarty
|
||||||
pysmarty2==0.10.2
|
pysmarty2==0.10.2
|
||||||
@ -1980,7 +1980,7 @@ python-juicenet==1.1.0
|
|||||||
python-kasa[speedups]==0.10.2
|
python-kasa[speedups]==0.10.2
|
||||||
|
|
||||||
# homeassistant.components.linkplay
|
# homeassistant.components.linkplay
|
||||||
python-linkplay==0.2.4
|
python-linkplay==0.2.5
|
||||||
|
|
||||||
# homeassistant.components.matter
|
# homeassistant.components.matter
|
||||||
python-matter-server==7.0.0
|
python-matter-server==7.0.0
|
||||||
@ -2023,7 +2023,7 @@ python-roborock==2.18.2
|
|||||||
python-smarttub==0.0.39
|
python-smarttub==0.0.39
|
||||||
|
|
||||||
# homeassistant.components.snoo
|
# homeassistant.components.snoo
|
||||||
python-snoo==0.6.5
|
python-snoo==0.6.6
|
||||||
|
|
||||||
# homeassistant.components.songpal
|
# homeassistant.components.songpal
|
||||||
python-songpal==0.16.2
|
python-songpal==0.16.2
|
||||||
@ -2144,7 +2144,7 @@ renault-api==0.3.1
|
|||||||
renson-endura-delta==1.7.2
|
renson-endura-delta==1.7.2
|
||||||
|
|
||||||
# homeassistant.components.reolink
|
# homeassistant.components.reolink
|
||||||
reolink-aio==0.13.2
|
reolink-aio==0.13.3
|
||||||
|
|
||||||
# homeassistant.components.rflink
|
# homeassistant.components.rflink
|
||||||
rflink==0.0.66
|
rflink==0.0.66
|
||||||
|
@ -246,6 +246,11 @@ aiofiles>=24.1.0
|
|||||||
# https://github.com/aio-libs/multidict/issues/1134
|
# https://github.com/aio-libs/multidict/issues/1134
|
||||||
# https://github.com/aio-libs/multidict/issues/1131
|
# https://github.com/aio-libs/multidict/issues/1131
|
||||||
multidict>=6.4.2
|
multidict>=6.4.2
|
||||||
|
|
||||||
|
# rpds-py > 0.25.0 requires cargo 1.84.0
|
||||||
|
# Stable Alpine current only ships cargo 1.83.0
|
||||||
|
# No wheels upstream available for armhf & armv7
|
||||||
|
rpds-py==0.24.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GENERATED_MESSAGE = (
|
GENERATED_MESSAGE = (
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
'current_temperature': 22.1,
|
'current_temperature': 22.1,
|
||||||
'friendly_name': 'Climate0',
|
'friendly_name': 'Climate0',
|
||||||
'hvac_action': <HVACAction.HEATING: 'heating'>,
|
'hvac_action': <HVACAction.IDLE: 'idle'>,
|
||||||
'hvac_modes': list([
|
'hvac_modes': list([
|
||||||
<HVACMode.AUTO: 'auto'>,
|
<HVACMode.AUTO: 'auto'>,
|
||||||
<HVACMode.COOL: 'cool'>,
|
<HVACMode.COOL: 'cool'>,
|
||||||
|
@ -181,14 +181,14 @@
|
|||||||
'supported_features': 0,
|
'supported_features': 0,
|
||||||
'translation_key': 'stats_area',
|
'translation_key': 'stats_area',
|
||||||
'unique_id': '8516fbb1-17f1-4194-0000000_stats_area',
|
'unique_id': '8516fbb1-17f1-4194-0000000_stats_area',
|
||||||
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
|
'unit_of_measurement': <UnitOfArea.SQUARE_CENTIMETERS: 'cm²'>,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned:state]
|
# name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned:state]
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
'friendly_name': 'Goat G1 Area cleaned',
|
'friendly_name': 'Goat G1 Area cleaned',
|
||||||
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
|
'unit_of_measurement': <UnitOfArea.SQUARE_CENTIMETERS: 'cm²'>,
|
||||||
}),
|
}),
|
||||||
'context': <ANY>,
|
'context': <ANY>,
|
||||||
'entity_id': 'sensor.goat_g1_area_cleaned',
|
'entity_id': 'sensor.goat_g1_area_cleaned',
|
||||||
@ -523,7 +523,7 @@
|
|||||||
'supported_features': 0,
|
'supported_features': 0,
|
||||||
'translation_key': 'total_stats_area',
|
'translation_key': 'total_stats_area',
|
||||||
'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_area',
|
'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_area',
|
||||||
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
|
'unit_of_measurement': <UnitOfArea.SQUARE_CENTIMETERS: 'cm²'>,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_sensors[5xu9h3][sensor.goat_g1_total_area_cleaned:state]
|
# name: test_sensors[5xu9h3][sensor.goat_g1_total_area_cleaned:state]
|
||||||
@ -531,7 +531,7 @@
|
|||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
'friendly_name': 'Goat G1 Total area cleaned',
|
'friendly_name': 'Goat G1 Total area cleaned',
|
||||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||||
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
|
'unit_of_measurement': <UnitOfArea.SQUARE_CENTIMETERS: 'cm²'>,
|
||||||
}),
|
}),
|
||||||
'context': <ANY>,
|
'context': <ANY>,
|
||||||
'entity_id': 'sensor.goat_g1_total_area_cleaned',
|
'entity_id': 'sensor.goat_g1_total_area_cleaned',
|
||||||
|
@ -896,8 +896,8 @@
|
|||||||
'/api/v1/production/inverters': 'Testing request replies.',
|
'/api/v1/production/inverters': 'Testing request replies.',
|
||||||
'/api/v1/production/inverters_log': '{"headers":{"Hello":"World"},"code":200}',
|
'/api/v1/production/inverters_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||||
'/api/v1/production_log': '{"headers":{"Hello":"World"},"code":200}',
|
'/api/v1/production_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||||
'/home,': 'Testing request replies.',
|
'/home': 'Testing request replies.',
|
||||||
'/home,_log': '{"headers":{"Hello":"World"},"code":200}',
|
'/home_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||||
'/info': 'Testing request replies.',
|
'/info': 'Testing request replies.',
|
||||||
'/info_log': '{"headers":{"Hello":"World"},"code":200}',
|
'/info_log': '{"headers":{"Hello":"World"},"code":200}',
|
||||||
'/ivp/ensemble/dry_contacts': 'Testing request replies.',
|
'/ivp/ensemble/dry_contacts': 'Testing request replies.',
|
||||||
@ -1390,7 +1390,7 @@
|
|||||||
'/api/v1/production_log': dict({
|
'/api/v1/production_log': dict({
|
||||||
'Error': "EnvoyError('Test')",
|
'Error': "EnvoyError('Test')",
|
||||||
}),
|
}),
|
||||||
'/home,_log': dict({
|
'/home_log': dict({
|
||||||
'Error': "EnvoyError('Test')",
|
'Error': "EnvoyError('Test')",
|
||||||
}),
|
}),
|
||||||
'/info_log': dict({
|
'/info_log': dict({
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Test ESPHome binary sensors."""
|
"""Test ESPHome binary sensors."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from dataclasses import asdict
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ from aioesphomeapi import (
|
|||||||
APIClient,
|
APIClient,
|
||||||
BinarySensorInfo,
|
BinarySensorInfo,
|
||||||
BinarySensorState,
|
BinarySensorState,
|
||||||
|
DeviceInfo,
|
||||||
SensorInfo,
|
SensorInfo,
|
||||||
SensorState,
|
SensorState,
|
||||||
build_unique_id,
|
build_unique_id,
|
||||||
@ -665,3 +667,63 @@ async def test_entity_id_preserved_on_upgrade_when_in_storage(
|
|||||||
)
|
)
|
||||||
state = hass.states.get("binary_sensor.user_named")
|
state = hass.states.get("binary_sensor.user_named")
|
||||||
assert state is not None
|
assert state is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_deep_sleep_added_after_setup(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_client: APIClient,
|
||||||
|
mock_esphome_device: MockESPHomeDeviceType,
|
||||||
|
) -> None:
|
||||||
|
"""Test deep sleep added after setup."""
|
||||||
|
mock_device = await mock_esphome_device(
|
||||||
|
mock_client=mock_client,
|
||||||
|
entity_info=[
|
||||||
|
BinarySensorInfo(
|
||||||
|
object_id="test",
|
||||||
|
key=1,
|
||||||
|
name="test",
|
||||||
|
unique_id="test",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
user_service=[],
|
||||||
|
states=[
|
||||||
|
BinarySensorState(key=1, state=True, missing_state=False),
|
||||||
|
],
|
||||||
|
device_info={"has_deep_sleep": False},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity_id = "binary_sensor.test_test"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
await mock_device.mock_disconnect(expected_disconnect=True)
|
||||||
|
|
||||||
|
# No deep sleep, should be unavailable
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
await mock_device.mock_connect()
|
||||||
|
|
||||||
|
# reconnect, should be available
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
await mock_device.mock_disconnect(expected_disconnect=True)
|
||||||
|
new_device_info = DeviceInfo(
|
||||||
|
**{**asdict(mock_device.device_info), "has_deep_sleep": True}
|
||||||
|
)
|
||||||
|
mock_device.client.device_info = AsyncMock(return_value=new_device_info)
|
||||||
|
mock_device.device_info = new_device_info
|
||||||
|
|
||||||
|
await mock_device.mock_connect()
|
||||||
|
|
||||||
|
# Now disconnect that deep sleep is set in device info
|
||||||
|
await mock_device.mock_disconnect(expected_disconnect=True)
|
||||||
|
|
||||||
|
# Deep sleep, should be available
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
@ -101,12 +101,12 @@ async def test_config_flow(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert result["title"] == "Home Assistant Yellow"
|
assert result["title"] == "Home Assistant Yellow"
|
||||||
assert result["data"] == {"firmware": "ezsp"}
|
assert result["data"] == {"firmware": "ezsp", "firmware_version": None}
|
||||||
assert result["options"] == {}
|
assert result["options"] == {}
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||||
assert config_entry.data == {"firmware": "ezsp"}
|
assert config_entry.data == {"firmware": "ezsp", "firmware_version": None}
|
||||||
assert config_entry.options == {}
|
assert config_entry.options == {}
|
||||||
assert config_entry.title == "Home Assistant Yellow"
|
assert config_entry.title == "Home Assistant Yellow"
|
||||||
|
|
||||||
|
@ -10,6 +10,9 @@ from homeassistant.components.homeassistant_hardware.util import (
|
|||||||
ApplicationType,
|
ApplicationType,
|
||||||
FirmwareInfo,
|
FirmwareInfo,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.homeassistant_yellow.config_flow import (
|
||||||
|
HomeAssistantYellowConfigFlow,
|
||||||
|
)
|
||||||
from homeassistant.components.homeassistant_yellow.const import DOMAIN
|
from homeassistant.components.homeassistant_yellow.const import DOMAIN
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -248,3 +251,71 @@ async def test_setup_entry_addon_info_fails(
|
|||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("start_version", "data", "migrated_data"),
|
||||||
|
[
|
||||||
|
(1, {}, {"firmware": "ezsp", "firmware_version": None}),
|
||||||
|
(2, {"firmware": "ezsp"}, {"firmware": "ezsp", "firmware_version": None}),
|
||||||
|
(
|
||||||
|
2,
|
||||||
|
{"firmware": "ezsp", "firmware_version": "123"},
|
||||||
|
{"firmware": "ezsp", "firmware_version": "123"},
|
||||||
|
),
|
||||||
|
(3, {"firmware": "ezsp"}, {"firmware": "ezsp", "firmware_version": None}),
|
||||||
|
(
|
||||||
|
3,
|
||||||
|
{"firmware": "ezsp", "firmware_version": "123"},
|
||||||
|
{"firmware": "ezsp", "firmware_version": "123"},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_migrate_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
start_version: int,
|
||||||
|
data: dict,
|
||||||
|
migrated_data: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Test migration of a config entry."""
|
||||||
|
mock_integration(hass, MockModule("hassio"))
|
||||||
|
await async_setup_component(hass, HASSIO_DOMAIN, {})
|
||||||
|
|
||||||
|
# Setup the config entry
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
data=data,
|
||||||
|
domain=DOMAIN,
|
||||||
|
options={},
|
||||||
|
title="Home Assistant Yellow",
|
||||||
|
version=1,
|
||||||
|
minor_version=start_version,
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.homeassistant_yellow.get_os_info",
|
||||||
|
return_value={"board": "yellow"},
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.onboarding.async_is_onboarded",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.homeassistant_yellow.guess_firmware_info",
|
||||||
|
return_value=FirmwareInfo( # Nothing is setup
|
||||||
|
device="/dev/ttyAMA1",
|
||||||
|
firmware_version="1234",
|
||||||
|
firmware_type=ApplicationType.EZSP,
|
||||||
|
source="unknown",
|
||||||
|
owners=[],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry.data == migrated_data
|
||||||
|
assert config_entry.options == {}
|
||||||
|
assert config_entry.minor_version == HomeAssistantYellowConfigFlow.MINOR_VERSION
|
||||||
|
assert config_entry.version == HomeAssistantYellowConfigFlow.VERSION
|
||||||
|
@ -35,3 +35,32 @@ async def test_door_bell_event(
|
|||||||
|
|
||||||
ha_state = hass.states.get(entity_id)
|
ha_state = hass.states.get(entity_id)
|
||||||
assert ha_state.state != STATE_UNKNOWN
|
assert ha_state.state != STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_door_bell_event_wrong_event_type(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
default_mock_hap_factory: HomeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test of door bell event of HmIP-DSD-PCB."""
|
||||||
|
entity_id = "event.dsdpcb_klingel_doorbell"
|
||||||
|
entity_name = "dsdpcb_klingel doorbell"
|
||||||
|
device_model = "HmIP-DSD-PCB"
|
||||||
|
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
|
||||||
|
test_devices=["dsdpcb_klingel"]
|
||||||
|
)
|
||||||
|
|
||||||
|
ha_state, hmip_device = get_and_check_entity_basics(
|
||||||
|
hass, mock_hap, entity_id, entity_name, device_model
|
||||||
|
)
|
||||||
|
|
||||||
|
ch = hmip_device.functionalChannels[1]
|
||||||
|
channel_event = ChannelEvent(
|
||||||
|
channelEventType="KEY_PRESS", channelIndex=1, deviceId=ch.device.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ha_state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
ch.fire_channel_event(channel_event)
|
||||||
|
|
||||||
|
ha_state = hass.states.get(entity_id)
|
||||||
|
assert ha_state.state == STATE_UNKNOWN
|
||||||
|
@ -37,6 +37,11 @@ from tests.common import MockConfigEntry, async_fire_time_changed
|
|||||||
MowerStates.IN_OPERATION,
|
MowerStates.IN_OPERATION,
|
||||||
LawnMowerActivity.MOWING,
|
LawnMowerActivity.MOWING,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
MowerActivities.PARKED_IN_CS,
|
||||||
|
MowerStates.IN_OPERATION,
|
||||||
|
LawnMowerActivity.DOCKED,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_lawn_mower_states(
|
async def test_lawn_mower_states(
|
||||||
|
@ -324,7 +324,6 @@ async def test_init_error(
|
|||||||
"type": "input_image",
|
"type": "input_image",
|
||||||
"image_url": "data:image/jpeg;base64,BASE64IMAGE1",
|
"image_url": "data:image/jpeg;base64,BASE64IMAGE1",
|
||||||
"detail": "auto",
|
"detail": "auto",
|
||||||
"file_id": "/a/b/c.jpg",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -349,13 +348,11 @@ async def test_init_error(
|
|||||||
"type": "input_image",
|
"type": "input_image",
|
||||||
"image_url": "data:image/jpeg;base64,BASE64IMAGE1",
|
"image_url": "data:image/jpeg;base64,BASE64IMAGE1",
|
||||||
"detail": "auto",
|
"detail": "auto",
|
||||||
"file_id": "/a/b/c.jpg",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "input_image",
|
"type": "input_image",
|
||||||
"image_url": "data:image/jpeg;base64,BASE64IMAGE2",
|
"image_url": "data:image/jpeg;base64,BASE64IMAGE2",
|
||||||
"detail": "auto",
|
"detail": "auto",
|
||||||
"file_id": "d/e/f.jpg",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -630,7 +630,7 @@ async def test_cleanup_mac_connection(
|
|||||||
domain = Platform.SWITCH
|
domain = Platform.SWITCH
|
||||||
|
|
||||||
dev_entry = device_registry.async_get_or_create(
|
dev_entry = device_registry.async_get_or_create(
|
||||||
identifiers={(DOMAIN, dev_id)},
|
identifiers={(DOMAIN, dev_id), ("OTHER_INTEGRATION", "SOME_ID")},
|
||||||
connections={(CONNECTION_NETWORK_MAC, TEST_MAC)},
|
connections={(CONNECTION_NETWORK_MAC, TEST_MAC)},
|
||||||
config_entry_id=config_entry.entry_id,
|
config_entry_id=config_entry.entry_id,
|
||||||
disabled_by=None,
|
disabled_by=None,
|
||||||
@ -664,6 +664,66 @@ async def test_cleanup_mac_connection(
|
|||||||
reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM
|
reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cleanup_combined_with_NVR(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
reolink_connect: MagicMock,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test cleanup of the device registry if IPC camera device was combined with the NVR device."""
|
||||||
|
reolink_connect.channels = [0]
|
||||||
|
reolink_connect.baichuan.mac_address.return_value = None
|
||||||
|
entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio"
|
||||||
|
dev_id = f"{TEST_UID}_{TEST_UID_CAM}"
|
||||||
|
domain = Platform.SWITCH
|
||||||
|
start_identifiers = {
|
||||||
|
(DOMAIN, dev_id),
|
||||||
|
(DOMAIN, TEST_UID),
|
||||||
|
("OTHER_INTEGRATION", "SOME_ID"),
|
||||||
|
}
|
||||||
|
|
||||||
|
dev_entry = device_registry.async_get_or_create(
|
||||||
|
identifiers=start_identifiers,
|
||||||
|
connections={(CONNECTION_NETWORK_MAC, TEST_MAC)},
|
||||||
|
config_entry_id=config_entry.entry_id,
|
||||||
|
disabled_by=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
entity_registry.async_get_or_create(
|
||||||
|
domain=domain,
|
||||||
|
platform=DOMAIN,
|
||||||
|
unique_id=entity_id,
|
||||||
|
config_entry=config_entry,
|
||||||
|
suggested_object_id=entity_id,
|
||||||
|
disabled_by=None,
|
||||||
|
device_id=dev_entry.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id)
|
||||||
|
device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)})
|
||||||
|
assert device
|
||||||
|
assert device.identifiers == start_identifiers
|
||||||
|
|
||||||
|
# setup CH 0 and host entities/device
|
||||||
|
with patch("homeassistant.components.reolink.PLATFORMS", [domain]):
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id)
|
||||||
|
device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)})
|
||||||
|
assert device
|
||||||
|
assert device.identifiers == {(DOMAIN, dev_id)}
|
||||||
|
host_device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_UID)})
|
||||||
|
assert host_device
|
||||||
|
assert host_device.identifiers == {
|
||||||
|
(DOMAIN, TEST_UID),
|
||||||
|
("OTHER_INTEGRATION", "SOME_ID"),
|
||||||
|
}
|
||||||
|
|
||||||
|
reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM
|
||||||
|
|
||||||
|
|
||||||
async def test_no_repair_issue(
|
async def test_no_repair_issue(
|
||||||
hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry
|
hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -11,7 +11,7 @@ import pytest
|
|||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
@ -45,3 +45,14 @@ async def test_sensor(
|
|||||||
|
|
||||||
state = hass.states.get("sensor.kitchen_pure_aqi")
|
state = hass.states.get("sensor.kitchen_pure_aqi")
|
||||||
assert state.state == "moderate"
|
assert state.state == "moderate"
|
||||||
|
|
||||||
|
mock_client.async_get_devices_data.return_value.parsed[
|
||||||
|
"AAZZAAZZ"
|
||||||
|
].pm25_pure = PureAQI(0)
|
||||||
|
|
||||||
|
freezer.tick(timedelta(minutes=5))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.kitchen_pure_aqi")
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
@ -150,6 +150,7 @@ def mock_smartthings() -> Generator[AsyncMock]:
|
|||||||
"generic_ef00_v1",
|
"generic_ef00_v1",
|
||||||
"bosch_radiator_thermostat_ii",
|
"bosch_radiator_thermostat_ii",
|
||||||
"im_speaker_ai_0001",
|
"im_speaker_ai_0001",
|
||||||
|
"im_smarttag2_ble_uwb",
|
||||||
"abl_light_b_001",
|
"abl_light_b_001",
|
||||||
"tplink_p110",
|
"tplink_p110",
|
||||||
"ikea_kadrilj",
|
"ikea_kadrilj",
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"timestamp": "2025-02-09T14:35:56.800Z"
|
"timestamp": "2025-02-09T14:35:56.800Z"
|
||||||
},
|
},
|
||||||
"supportedAcModes": {
|
"supportedAcModes": {
|
||||||
"value": ["auto", "cool", "dry", "wind", "heat", "dryClean"],
|
"value": ["auto", "cool", "dry", "fan", "heat", "dryClean"],
|
||||||
"timestamp": "2025-02-09T15:42:13.444Z"
|
"timestamp": "2025-02-09T15:42:13.444Z"
|
||||||
},
|
},
|
||||||
"airConditionerMode": {
|
"airConditionerMode": {
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"components": {
|
||||||
|
"main": {
|
||||||
|
"tag.e2eEncryption": {
|
||||||
|
"encryption": {
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"audioVolume": {
|
||||||
|
"volume": {
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geofence": {
|
||||||
|
"enableState": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"geofence": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tag.updatedInfo": {
|
||||||
|
"connection": {
|
||||||
|
"value": "connected",
|
||||||
|
"timestamp": "2024-02-27T17:44:57.638Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tag.factoryReset": {},
|
||||||
|
"battery": {
|
||||||
|
"quantity": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"battery": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"firmwareUpdate": {
|
||||||
|
"lastUpdateStatusReason": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"availableVersion": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"lastUpdateStatus": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"supportedCommands": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"updateAvailable": {
|
||||||
|
"value": false,
|
||||||
|
"timestamp": "2024-06-25T05:56:22.227Z"
|
||||||
|
},
|
||||||
|
"currentVersion": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"lastUpdateTime": {
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tag.searchingStatus": {
|
||||||
|
"searchingStatus": {
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tag.tagStatus": {
|
||||||
|
"connectedUserId": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"tagStatus": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"connectedDeviceId": {
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"alarm": {
|
||||||
|
"alarm": {
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tag.tagButton": {
|
||||||
|
"tagButton": {
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tag.uwbActivation": {
|
||||||
|
"uwbActivation": {
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geolocation": {
|
||||||
|
"method": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"heading": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"latitude": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"accuracy": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"altitudeAccuracy": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"speed": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"longitude": {
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
"lastUpdateTime": {
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,184 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"deviceId": "83d660e4-b0c8-4881-a674-d9f1730366c1",
|
||||||
|
"name": "Tag(UWB)",
|
||||||
|
"label": "SmartTag+ black",
|
||||||
|
"manufacturerName": "Samsung Electronics",
|
||||||
|
"presentationId": "IM-SmartTag-BLE-UWB",
|
||||||
|
"deviceManufacturerCode": "Samsung Electronics",
|
||||||
|
"locationId": "redacted_locid",
|
||||||
|
"ownerId": "redacted",
|
||||||
|
"roomId": "redacted_roomid",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"id": "main",
|
||||||
|
"label": "main",
|
||||||
|
"capabilities": [
|
||||||
|
{
|
||||||
|
"id": "alarm",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tag.tagButton",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audioVolume",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "battery",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tag.factoryReset",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tag.e2eEncryption",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tag.tagStatus",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "geolocation",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "geofence",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tag.uwbActivation",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tag.updatedInfo",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tag.searchingStatus",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firmwareUpdate",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "BluetoothTracker",
|
||||||
|
"categoryType": "manufacturer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createTime": "2023-05-25T09:42:59.720Z",
|
||||||
|
"profile": {
|
||||||
|
"id": "e443f3e8-a926-3deb-917c-e5c6de3af70f"
|
||||||
|
},
|
||||||
|
"bleD2D": {
|
||||||
|
"encryptionKey": "ZTbd_04NISrhQODE7_i8JdcG2ZWwqmUfY60taptK7J0=",
|
||||||
|
"cipher": "AES_128-CBC-PKCS7Padding",
|
||||||
|
"identifier": "415D4Y16F97F",
|
||||||
|
"configurationVersion": "2.0",
|
||||||
|
"configurationUrl": "https://apis.samsungiotcloud.com/v1/miniature/profile/b8e65e7e-6152-4704-b9f5-f16352034237",
|
||||||
|
"bleDeviceType": "BLE",
|
||||||
|
"metadata": {
|
||||||
|
"regionCode": 11,
|
||||||
|
"privacyIdPoolSize": 2000,
|
||||||
|
"privacyIdSeed": "AAAAAAAX8IQ=",
|
||||||
|
"privacyIdInitialVector": "ZfqZKLRGSeCwgNhdqHFRpw==",
|
||||||
|
"numAllowableConnections": 2,
|
||||||
|
"firmware": {
|
||||||
|
"version": "1.03.07",
|
||||||
|
"specVersion": "0.5.6",
|
||||||
|
"updateTime": 1685007914000,
|
||||||
|
"latestFirmware": {
|
||||||
|
"id": 581,
|
||||||
|
"version": "1.03.07",
|
||||||
|
"data": {
|
||||||
|
"checksum": "50E7",
|
||||||
|
"size": "586004",
|
||||||
|
"supportedVersion": "0.5.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"currentServerTime": 1739095473,
|
||||||
|
"searchingStatus": "stop",
|
||||||
|
"lastKnownConnection": {
|
||||||
|
"updated": 1713422813,
|
||||||
|
"connectedUser": {
|
||||||
|
"id": "sk3oyvsbkm",
|
||||||
|
"name": ""
|
||||||
|
},
|
||||||
|
"connectedDevice": {
|
||||||
|
"id": "4f3faa4c-976c-3bd8-b209-607f3a5a9814",
|
||||||
|
"name": ""
|
||||||
|
},
|
||||||
|
"d2dStatus": "bleScanned",
|
||||||
|
"nearby": true,
|
||||||
|
"onDemand": false
|
||||||
|
},
|
||||||
|
"e2eEncryption": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"timer": 1713422675,
|
||||||
|
"category": {
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"remoteRing": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"petWalking": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"onboardedBy": {
|
||||||
|
"saGuid": "sk3oyvsbkm"
|
||||||
|
},
|
||||||
|
"shareable": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"agingCounter": {
|
||||||
|
"status": "VALID",
|
||||||
|
"updated": 1713422675
|
||||||
|
},
|
||||||
|
"vendor": {
|
||||||
|
"mnId": "0AFD",
|
||||||
|
"setupId": "432",
|
||||||
|
"modelName": "EI-T7300"
|
||||||
|
},
|
||||||
|
"priorityConnection": {
|
||||||
|
"lba": false,
|
||||||
|
"cameraShutter": false
|
||||||
|
},
|
||||||
|
"createTime": 1685007780,
|
||||||
|
"updateTime": 1713422675,
|
||||||
|
"fmmSearch": false,
|
||||||
|
"ooTime": {
|
||||||
|
"currentOoTime": 8,
|
||||||
|
"defaultOoTime": 8
|
||||||
|
},
|
||||||
|
"pidPoolSize": {
|
||||||
|
"desiredPidPoolSize": 2000,
|
||||||
|
"currentPidPoolSize": 2000
|
||||||
|
},
|
||||||
|
"activeMode": {
|
||||||
|
"mode": 0
|
||||||
|
},
|
||||||
|
"itemConfig": {
|
||||||
|
"searchingStatus": "stop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "BLE_D2D",
|
||||||
|
"restrictionTier": 0,
|
||||||
|
"allowed": [],
|
||||||
|
"executionContext": "CLOUD"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"_links": {}
|
||||||
|
}
|
@ -146,7 +146,7 @@
|
|||||||
<HVACMode.COOL: 'cool'>,
|
<HVACMode.COOL: 'cool'>,
|
||||||
<HVACMode.DRY: 'dry'>,
|
<HVACMode.DRY: 'dry'>,
|
||||||
<HVACMode.FAN_ONLY: 'fan_only'>,
|
<HVACMode.FAN_ONLY: 'fan_only'>,
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.AUTO: 'auto'>,
|
||||||
<HVACMode.HEAT: 'heat'>,
|
<HVACMode.HEAT: 'heat'>,
|
||||||
]),
|
]),
|
||||||
'max_temp': 35,
|
'max_temp': 35,
|
||||||
@ -206,7 +206,7 @@
|
|||||||
<HVACMode.COOL: 'cool'>,
|
<HVACMode.COOL: 'cool'>,
|
||||||
<HVACMode.DRY: 'dry'>,
|
<HVACMode.DRY: 'dry'>,
|
||||||
<HVACMode.FAN_ONLY: 'fan_only'>,
|
<HVACMode.FAN_ONLY: 'fan_only'>,
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.AUTO: 'auto'>,
|
||||||
<HVACMode.HEAT: 'heat'>,
|
<HVACMode.HEAT: 'heat'>,
|
||||||
]),
|
]),
|
||||||
'max_temp': 35,
|
'max_temp': 35,
|
||||||
@ -246,7 +246,7 @@
|
|||||||
<HVACMode.COOL: 'cool'>,
|
<HVACMode.COOL: 'cool'>,
|
||||||
<HVACMode.DRY: 'dry'>,
|
<HVACMode.DRY: 'dry'>,
|
||||||
<HVACMode.FAN_ONLY: 'fan_only'>,
|
<HVACMode.FAN_ONLY: 'fan_only'>,
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.AUTO: 'auto'>,
|
||||||
]),
|
]),
|
||||||
'max_temp': 35,
|
'max_temp': 35,
|
||||||
'min_temp': 7,
|
'min_temp': 7,
|
||||||
@ -308,7 +308,7 @@
|
|||||||
<HVACMode.COOL: 'cool'>,
|
<HVACMode.COOL: 'cool'>,
|
||||||
<HVACMode.DRY: 'dry'>,
|
<HVACMode.DRY: 'dry'>,
|
||||||
<HVACMode.FAN_ONLY: 'fan_only'>,
|
<HVACMode.FAN_ONLY: 'fan_only'>,
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.AUTO: 'auto'>,
|
||||||
]),
|
]),
|
||||||
'max_temp': 35,
|
'max_temp': 35,
|
||||||
'min_temp': 7,
|
'min_temp': 7,
|
||||||
@ -349,7 +349,7 @@
|
|||||||
]),
|
]),
|
||||||
'hvac_modes': list([
|
'hvac_modes': list([
|
||||||
<HVACMode.OFF: 'off'>,
|
<HVACMode.OFF: 'off'>,
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.AUTO: 'auto'>,
|
||||||
<HVACMode.COOL: 'cool'>,
|
<HVACMode.COOL: 'cool'>,
|
||||||
<HVACMode.DRY: 'dry'>,
|
<HVACMode.DRY: 'dry'>,
|
||||||
<HVACMode.FAN_ONLY: 'fan_only'>,
|
<HVACMode.FAN_ONLY: 'fan_only'>,
|
||||||
@ -414,7 +414,7 @@
|
|||||||
'friendly_name': 'Aire Dormitorio Principal',
|
'friendly_name': 'Aire Dormitorio Principal',
|
||||||
'hvac_modes': list([
|
'hvac_modes': list([
|
||||||
<HVACMode.OFF: 'off'>,
|
<HVACMode.OFF: 'off'>,
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.AUTO: 'auto'>,
|
||||||
<HVACMode.COOL: 'cool'>,
|
<HVACMode.COOL: 'cool'>,
|
||||||
<HVACMode.DRY: 'dry'>,
|
<HVACMode.DRY: 'dry'>,
|
||||||
<HVACMode.FAN_ONLY: 'fan_only'>,
|
<HVACMode.FAN_ONLY: 'fan_only'>,
|
||||||
@ -462,7 +462,7 @@
|
|||||||
<HVACMode.COOL: 'cool'>,
|
<HVACMode.COOL: 'cool'>,
|
||||||
<HVACMode.DRY: 'dry'>,
|
<HVACMode.DRY: 'dry'>,
|
||||||
<HVACMode.FAN_ONLY: 'fan_only'>,
|
<HVACMode.FAN_ONLY: 'fan_only'>,
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.AUTO: 'auto'>,
|
||||||
]),
|
]),
|
||||||
'max_temp': 35,
|
'max_temp': 35,
|
||||||
'min_temp': 7,
|
'min_temp': 7,
|
||||||
@ -513,7 +513,7 @@
|
|||||||
<HVACMode.COOL: 'cool'>,
|
<HVACMode.COOL: 'cool'>,
|
||||||
<HVACMode.DRY: 'dry'>,
|
<HVACMode.DRY: 'dry'>,
|
||||||
<HVACMode.FAN_ONLY: 'fan_only'>,
|
<HVACMode.FAN_ONLY: 'fan_only'>,
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.AUTO: 'auto'>,
|
||||||
]),
|
]),
|
||||||
'max_temp': 35,
|
'max_temp': 35,
|
||||||
'min_temp': 7,
|
'min_temp': 7,
|
||||||
@ -541,7 +541,7 @@
|
|||||||
'hvac_modes': list([
|
'hvac_modes': list([
|
||||||
<HVACMode.OFF: 'off'>,
|
<HVACMode.OFF: 'off'>,
|
||||||
<HVACMode.COOL: 'cool'>,
|
<HVACMode.COOL: 'cool'>,
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.AUTO: 'auto'>,
|
||||||
]),
|
]),
|
||||||
'max_temp': 35.0,
|
'max_temp': 35.0,
|
||||||
'min_temp': 7.0,
|
'min_temp': 7.0,
|
||||||
@ -589,7 +589,7 @@
|
|||||||
'hvac_modes': list([
|
'hvac_modes': list([
|
||||||
<HVACMode.OFF: 'off'>,
|
<HVACMode.OFF: 'off'>,
|
||||||
<HVACMode.COOL: 'cool'>,
|
<HVACMode.COOL: 'cool'>,
|
||||||
<HVACMode.HEAT_COOL: 'heat_cool'>,
|
<HVACMode.AUTO: 'auto'>,
|
||||||
]),
|
]),
|
||||||
'max_temp': 35.0,
|
'max_temp': 35.0,
|
||||||
'min_temp': 7.0,
|
'min_temp': 7.0,
|
||||||
|
@ -1487,6 +1487,39 @@
|
|||||||
'via_device_id': None,
|
'via_device_id': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_devices[im_smarttag2_ble_uwb]
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': None,
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'config_entries_subentries': <ANY>,
|
||||||
|
'configuration_url': 'https://account.smartthings.com',
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': None,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'smartthings',
|
||||||
|
'83d660e4-b0c8-4881-a674-d9f1730366c1',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'is_new': False,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'manufacturer': None,
|
||||||
|
'model': None,
|
||||||
|
'model_id': None,
|
||||||
|
'name': 'SmartTag+ black',
|
||||||
|
'name_by_user': None,
|
||||||
|
'primary_config_entry': <ANY>,
|
||||||
|
'serial_number': None,
|
||||||
|
'suggested_area': None,
|
||||||
|
'sw_version': None,
|
||||||
|
'via_device_id': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_devices[im_speaker_ai_0001]
|
# name: test_devices[im_speaker_ai_0001]
|
||||||
DeviceRegistryEntrySnapshot({
|
DeviceRegistryEntrySnapshot({
|
||||||
'area_id': None,
|
'area_id': None,
|
||||||
|
@ -119,7 +119,7 @@ async def test_ac_set_hvac_mode_off(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("hvac_mode", "argument"),
|
("hvac_mode", "argument"),
|
||||||
[
|
[
|
||||||
(HVACMode.HEAT_COOL, "auto"),
|
(HVACMode.AUTO, "auto"),
|
||||||
(HVACMode.COOL, "cool"),
|
(HVACMode.COOL, "cool"),
|
||||||
(HVACMode.DRY, "dry"),
|
(HVACMode.DRY, "dry"),
|
||||||
(HVACMode.HEAT, "heat"),
|
(HVACMode.HEAT, "heat"),
|
||||||
@ -174,7 +174,7 @@ async def test_ac_set_hvac_mode_turns_on(
|
|||||||
SERVICE_SET_HVAC_MODE,
|
SERVICE_SET_HVAC_MODE,
|
||||||
{
|
{
|
||||||
ATTR_ENTITY_ID: "climate.ac_office_granit",
|
ATTR_ENTITY_ID: "climate.ac_office_granit",
|
||||||
ATTR_HVAC_MODE: HVACMode.HEAT_COOL,
|
ATTR_HVAC_MODE: HVACMode.AUTO,
|
||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
@ -196,17 +196,19 @@ async def test_ac_set_hvac_mode_turns_on(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||||
async def test_ac_set_hvac_mode_wind(
|
@pytest.mark.parametrize("mode", ["fan", "wind"])
|
||||||
|
async def test_ac_set_hvac_mode_fan(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
devices: AsyncMock,
|
devices: AsyncMock,
|
||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mode: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test setting AC HVAC mode to wind if the device supports it."""
|
"""Test setting AC HVAC mode to wind if the device supports it."""
|
||||||
set_attribute_value(
|
set_attribute_value(
|
||||||
devices,
|
devices,
|
||||||
Capability.AIR_CONDITIONER_MODE,
|
Capability.AIR_CONDITIONER_MODE,
|
||||||
Attribute.SUPPORTED_AC_MODES,
|
Attribute.SUPPORTED_AC_MODES,
|
||||||
["auto", "cool", "dry", "heat", "wind"],
|
["auto", "cool", "dry", "heat", mode],
|
||||||
)
|
)
|
||||||
set_attribute_value(devices, Capability.SWITCH, Attribute.SWITCH, "on")
|
set_attribute_value(devices, Capability.SWITCH, Attribute.SWITCH, "on")
|
||||||
|
|
||||||
@ -223,7 +225,7 @@ async def test_ac_set_hvac_mode_wind(
|
|||||||
Capability.AIR_CONDITIONER_MODE,
|
Capability.AIR_CONDITIONER_MODE,
|
||||||
Command.SET_AIR_CONDITIONER_MODE,
|
Command.SET_AIR_CONDITIONER_MODE,
|
||||||
MAIN,
|
MAIN,
|
||||||
argument="wind",
|
argument=mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -266,7 +268,7 @@ async def test_ac_set_temperature_and_hvac_mode_while_off(
|
|||||||
{
|
{
|
||||||
ATTR_ENTITY_ID: "climate.ac_office_granit",
|
ATTR_ENTITY_ID: "climate.ac_office_granit",
|
||||||
ATTR_TEMPERATURE: 23,
|
ATTR_TEMPERATURE: 23,
|
||||||
ATTR_HVAC_MODE: HVACMode.HEAT_COOL,
|
ATTR_HVAC_MODE: HVACMode.AUTO,
|
||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
@ -316,7 +318,7 @@ async def test_ac_set_temperature_and_hvac_mode(
|
|||||||
{
|
{
|
||||||
ATTR_ENTITY_ID: "climate.ac_office_granit",
|
ATTR_ENTITY_ID: "climate.ac_office_granit",
|
||||||
ATTR_TEMPERATURE: 23,
|
ATTR_TEMPERATURE: 23,
|
||||||
ATTR_HVAC_MODE: HVACMode.HEAT_COOL,
|
ATTR_HVAC_MODE: HVACMode.AUTO,
|
||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
@ -623,7 +625,7 @@ async def test_thermostat_set_hvac_mode(
|
|||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
CLIMATE_DOMAIN,
|
CLIMATE_DOMAIN,
|
||||||
SERVICE_SET_HVAC_MODE,
|
SERVICE_SET_HVAC_MODE,
|
||||||
{ATTR_ENTITY_ID: "climate.asd", ATTR_HVAC_MODE: HVACMode.HEAT_COOL},
|
{ATTR_ENTITY_ID: "climate.asd", ATTR_HVAC_MODE: HVACMode.AUTO},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
devices.execute_device_command.assert_called_once_with(
|
devices.execute_device_command.assert_called_once_with(
|
||||||
|
@ -59,6 +59,37 @@ async def test_devices(
|
|||||||
assert device == snapshot
|
assert device == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"])
|
||||||
|
async def test_device_not_resetting_area(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
devices: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test device not resetting area."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
device_id = devices.get_devices.return_value[0].device_id
|
||||||
|
|
||||||
|
device = device_registry.async_get_device({(DOMAIN, device_id)})
|
||||||
|
|
||||||
|
assert device.area_id == "theater"
|
||||||
|
|
||||||
|
device_registry.async_update_device(device_id=device.id, area_id=None)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
device = device_registry.async_get_device({(DOMAIN, device_id)})
|
||||||
|
|
||||||
|
assert device.area_id is None
|
||||||
|
|
||||||
|
await hass.config_entries.async_reload(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
device = device_registry.async_get_device({(DOMAIN, device_id)})
|
||||||
|
assert device.area_id is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("device_fixture", ["button"])
|
@pytest.mark.parametrize("device_fixture", ["button"])
|
||||||
async def test_button_event(
|
async def test_button_event(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -21,6 +21,7 @@ from tests.common import (
|
|||||||
|
|
||||||
MOCK_DEVICE_NAME = "slzb-06"
|
MOCK_DEVICE_NAME = "slzb-06"
|
||||||
MOCK_HOST = "192.168.1.161"
|
MOCK_HOST = "192.168.1.161"
|
||||||
|
MOCK_HOSTNAME = "slzb-06p7.lan"
|
||||||
MOCK_USERNAME = "test-user"
|
MOCK_USERNAME = "test-user"
|
||||||
MOCK_PASSWORD = "test-pass"
|
MOCK_PASSWORD = "test-pass"
|
||||||
|
|
||||||
|
@ -15,7 +15,13 @@ from homeassistant.data_entry_flow import FlowResultType
|
|||||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
|
||||||
from .conftest import MOCK_DEVICE_NAME, MOCK_HOST, MOCK_PASSWORD, MOCK_USERNAME
|
from .conftest import (
|
||||||
|
MOCK_DEVICE_NAME,
|
||||||
|
MOCK_HOST,
|
||||||
|
MOCK_HOSTNAME,
|
||||||
|
MOCK_PASSWORD,
|
||||||
|
MOCK_USERNAME,
|
||||||
|
)
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -53,14 +59,14 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No
|
|||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{
|
{
|
||||||
CONF_HOST: "slzb-06p7.local",
|
CONF_HOST: MOCK_HOSTNAME,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert result2["title"] == "SLZB-06p7"
|
assert result2["title"] == "SLZB-06p7"
|
||||||
assert result2["data"] == {
|
assert result2["data"] == {
|
||||||
CONF_HOST: MOCK_HOST,
|
CONF_HOST: MOCK_HOSTNAME,
|
||||||
}
|
}
|
||||||
assert result2["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff"
|
assert result2["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff"
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
@ -82,7 +88,7 @@ async def test_user_flow_auth(
|
|||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{
|
{
|
||||||
CONF_HOST: "slzb-06p7.local",
|
CONF_HOST: MOCK_HOSTNAME,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert result2["type"] is FlowResultType.FORM
|
assert result2["type"] is FlowResultType.FORM
|
||||||
@ -100,7 +106,7 @@ async def test_user_flow_auth(
|
|||||||
assert result3["data"] == {
|
assert result3["data"] == {
|
||||||
CONF_USERNAME: MOCK_USERNAME,
|
CONF_USERNAME: MOCK_USERNAME,
|
||||||
CONF_PASSWORD: MOCK_PASSWORD,
|
CONF_PASSWORD: MOCK_PASSWORD,
|
||||||
CONF_HOST: MOCK_HOST,
|
CONF_HOST: MOCK_HOSTNAME,
|
||||||
}
|
}
|
||||||
assert result3["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff"
|
assert result3["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff"
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
@ -4978,7 +4978,7 @@
|
|||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': 'unknown',
|
'state': 'disconnected',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_sensors[sensor.wall_connector_vehicle-statealt]
|
# name: test_sensors[sensor.wall_connector_vehicle-statealt]
|
||||||
@ -4991,7 +4991,7 @@
|
|||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': 'unknown',
|
'state': 'disconnected',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_sensors[sensor.wall_connector_vehicle_2-entry]
|
# name: test_sensors[sensor.wall_connector_vehicle_2-entry]
|
||||||
@ -5038,7 +5038,7 @@
|
|||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': 'unknown',
|
'state': 'disconnected',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_sensors[sensor.wall_connector_vehicle_2-statealt]
|
# name: test_sensors[sensor.wall_connector_vehicle_2-statealt]
|
||||||
@ -5051,7 +5051,7 @@
|
|||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': 'unknown',
|
'state': 'disconnected',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_sensors_streaming[sensor.test_battery_level-state]
|
# name: test_sensors_streaming[sensor.test_battery_level-state]
|
||||||
|
@ -258,3 +258,67 @@ async def test_invalid_zha_event_type(
|
|||||||
# `zha_send_event` accepts only zigpy responses, lists, and dicts
|
# `zha_send_event` accepts only zigpy responses, lists, and dicts
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
cluster_handler.zha_send_event(COMMAND_SINGLE, 123)
|
cluster_handler.zha_send_event(COMMAND_SINGLE, 123)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_client_unique_id_suffix_stripped(
|
||||||
|
hass: HomeAssistant, setup_zha, zigpy_device_mock
|
||||||
|
) -> None:
|
||||||
|
"""Test that the `_CLIENT_` unique ID suffix is stripped."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"trigger": {
|
||||||
|
"platform": "event",
|
||||||
|
"event_type": "zha_event",
|
||||||
|
"event_data": {
|
||||||
|
"unique_id": "38:5b:44:ff:fe:a7:cc:69:1:0x0006", # no `_CLIENT` suffix
|
||||||
|
"endpoint_id": 1,
|
||||||
|
"cluster_id": 6,
|
||||||
|
"command": "on",
|
||||||
|
"args": [],
|
||||||
|
"params": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"action": {"service": "zha.test"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
service_calls = async_mock_service(hass, DOMAIN, "test")
|
||||||
|
|
||||||
|
await setup_zha()
|
||||||
|
gateway = get_zha_gateway(hass)
|
||||||
|
|
||||||
|
zigpy_device = zigpy_device_mock(
|
||||||
|
{
|
||||||
|
1: {
|
||||||
|
SIG_EP_INPUT: [
|
||||||
|
general.Basic.cluster_id,
|
||||||
|
security.IasZone.cluster_id,
|
||||||
|
security.IasWd.cluster_id,
|
||||||
|
],
|
||||||
|
SIG_EP_OUTPUT: [general.OnOff.cluster_id],
|
||||||
|
SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH,
|
||||||
|
SIG_EP_PROFILE: zha.PROFILE_ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
zha_device = gateway.get_or_create_device(zigpy_device)
|
||||||
|
await gateway.async_device_initialized(zha_device.device)
|
||||||
|
|
||||||
|
zha_device.emit_zha_event(
|
||||||
|
{
|
||||||
|
"unique_id": "38:5b:44:ff:fe:a7:cc:69:1:0x0006_CLIENT",
|
||||||
|
"endpoint_id": 1,
|
||||||
|
"cluster_id": 6,
|
||||||
|
"command": "on",
|
||||||
|
"args": [],
|
||||||
|
"params": {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
assert len(service_calls) == 1
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Provide common Z-Wave JS fixtures."""
|
"""Provide common Z-Wave JS fixtures."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Generator
|
||||||
import copy
|
import copy
|
||||||
import io
|
import io
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
@ -15,6 +16,7 @@ from zwave_js_server.version import VersionInfo
|
|||||||
|
|
||||||
from homeassistant.components.zwave_js import PLATFORMS
|
from homeassistant.components.zwave_js import PLATFORMS
|
||||||
from homeassistant.components.zwave_js.const import DOMAIN
|
from homeassistant.components.zwave_js.const import DOMAIN
|
||||||
|
from homeassistant.components.zwave_js.helpers import SERVER_VERSION_TIMEOUT
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.util.json import JsonArrayType
|
from homeassistant.util.json import JsonArrayType
|
||||||
@ -587,6 +589,44 @@ def mock_client_fixture(
|
|||||||
yield client
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="server_version_side_effect")
|
||||||
|
def server_version_side_effect_fixture() -> Any | None:
|
||||||
|
"""Return the server version side effect."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="get_server_version", autouse=True)
|
||||||
|
def mock_get_server_version(
|
||||||
|
server_version_side_effect: Any | None, server_version_timeout: int
|
||||||
|
) -> Generator[AsyncMock]:
|
||||||
|
"""Mock server version."""
|
||||||
|
version_info = VersionInfo(
|
||||||
|
driver_version="mock-driver-version",
|
||||||
|
server_version="mock-server-version",
|
||||||
|
home_id=1234,
|
||||||
|
min_schema_version=0,
|
||||||
|
max_schema_version=1,
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.zwave_js.helpers.get_server_version",
|
||||||
|
side_effect=server_version_side_effect,
|
||||||
|
return_value=version_info,
|
||||||
|
) as mock_version,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.zwave_js.helpers.SERVER_VERSION_TIMEOUT",
|
||||||
|
new=server_version_timeout,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
yield mock_version
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="server_version_timeout")
|
||||||
|
def mock_server_version_timeout() -> int:
|
||||||
|
"""Patch the timeout for getting server version."""
|
||||||
|
return SERVER_VERSION_TIMEOUT
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="multisensor_6")
|
@pytest.fixture(name="multisensor_6")
|
||||||
def multisensor_6_fixture(client, multisensor_6_state) -> Node:
|
def multisensor_6_fixture(client, multisensor_6_state) -> Node:
|
||||||
"""Mock a multisensor 6 node."""
|
"""Mock a multisensor 6 node."""
|
||||||
@ -843,7 +883,11 @@ async def integration_fixture(
|
|||||||
platforms: list[Platform],
|
platforms: list[Platform],
|
||||||
) -> MockConfigEntry:
|
) -> MockConfigEntry:
|
||||||
"""Set up the zwave_js integration."""
|
"""Set up the zwave_js integration."""
|
||||||
entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
|
entry = MockConfigEntry(
|
||||||
|
domain="zwave_js",
|
||||||
|
data={"url": "ws://test.org"},
|
||||||
|
unique_id=str(client.driver.controller.home_id),
|
||||||
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
with patch("homeassistant.components.zwave_js.PLATFORMS", platforms):
|
with patch("homeassistant.components.zwave_js.PLATFORMS", platforms):
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
@ -7,6 +7,7 @@ import json
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock, PropertyMock, call, patch
|
from unittest.mock import AsyncMock, MagicMock, PropertyMock, call, patch
|
||||||
|
|
||||||
|
from aiohttp import ClientError
|
||||||
import pytest
|
import pytest
|
||||||
from zwave_js_server.const import (
|
from zwave_js_server.const import (
|
||||||
ExclusionStrategy,
|
ExclusionStrategy,
|
||||||
@ -5080,14 +5081,17 @@ async def test_subscribe_node_statistics(
|
|||||||
|
|
||||||
async def test_hard_reset_controller(
|
async def test_hard_reset_controller(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
device_registry: dr.DeviceRegistry,
|
device_registry: dr.DeviceRegistry,
|
||||||
client: MagicMock,
|
client: MagicMock,
|
||||||
|
get_server_version: AsyncMock,
|
||||||
integration: MockConfigEntry,
|
integration: MockConfigEntry,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that the hard_reset_controller WS API call works."""
|
"""Test that the hard_reset_controller WS API call works."""
|
||||||
entry = integration
|
entry = integration
|
||||||
ws_client = await hass_ws_client(hass)
|
ws_client = await hass_ws_client(hass)
|
||||||
|
assert entry.unique_id == "3245146787"
|
||||||
|
|
||||||
async def async_send_command_driver_ready(
|
async def async_send_command_driver_ready(
|
||||||
message: dict[str, Any],
|
message: dict[str, Any],
|
||||||
@ -5122,6 +5126,40 @@ async def test_hard_reset_controller(
|
|||||||
assert client.async_send_command.call_args_list[0] == call(
|
assert client.async_send_command.call_args_list[0] == call(
|
||||||
{"command": "driver.hard_reset"}, 25
|
{"command": "driver.hard_reset"}, 25
|
||||||
)
|
)
|
||||||
|
assert entry.unique_id == "1234"
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test client connect error when getting the server version.
|
||||||
|
|
||||||
|
get_server_version.side_effect = ClientError("Boom!")
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
TYPE: "zwave_js/hard_reset_controller",
|
||||||
|
ENTRY_ID: entry.entry_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
|
||||||
|
device = device_registry.async_get_device(
|
||||||
|
identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])}
|
||||||
|
)
|
||||||
|
assert device is not None
|
||||||
|
assert msg["result"] == device.id
|
||||||
|
assert msg["success"]
|
||||||
|
|
||||||
|
assert client.async_send_command.call_count == 3
|
||||||
|
# The first call is the relevant hard reset command.
|
||||||
|
# 25 is the require_schema parameter.
|
||||||
|
assert client.async_send_command.call_args_list[0] == call(
|
||||||
|
{"command": "driver.hard_reset"}, 25
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
"Failed to get server version, cannot update config entry"
|
||||||
|
"unique id with new home id, after controller reset"
|
||||||
|
) in caplog.text
|
||||||
|
|
||||||
client.async_send_command.reset_mock()
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
@ -5162,6 +5200,8 @@ async def test_hard_reset_controller(
|
|||||||
{"command": "driver.hard_reset"}, 25
|
{"command": "driver.hard_reset"}, 25
|
||||||
)
|
)
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
# Test FailedZWaveCommand is caught
|
# Test FailedZWaveCommand is caught
|
||||||
with patch(
|
with patch(
|
||||||
"zwave_js_server.model.driver.Driver.async_hard_reset",
|
"zwave_js_server.model.driver.Driver.async_hard_reset",
|
||||||
|
@ -17,8 +17,9 @@ from zwave_js_server.exceptions import FailedCommand
|
|||||||
from zwave_js_server.version import VersionInfo
|
from zwave_js_server.version import VersionInfo
|
||||||
|
|
||||||
from homeassistant import config_entries, data_entry_flow
|
from homeassistant import config_entries, data_entry_flow
|
||||||
from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE
|
from homeassistant.components.zwave_js.config_flow import TITLE
|
||||||
from homeassistant.components.zwave_js.const import ADDON_SLUG, CONF_USB_PATH, DOMAIN
|
from homeassistant.components.zwave_js.const import ADDON_SLUG, CONF_USB_PATH, DOMAIN
|
||||||
|
from homeassistant.components.zwave_js.helpers import SERVER_VERSION_TIMEOUT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||||
@ -89,44 +90,6 @@ def mock_supervisor_fixture() -> Generator[None]:
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="server_version_side_effect")
|
|
||||||
def server_version_side_effect_fixture() -> Any | None:
|
|
||||||
"""Return the server version side effect."""
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="get_server_version", autouse=True)
|
|
||||||
def mock_get_server_version(
|
|
||||||
server_version_side_effect: Any | None, server_version_timeout: int
|
|
||||||
) -> Generator[AsyncMock]:
|
|
||||||
"""Mock server version."""
|
|
||||||
version_info = VersionInfo(
|
|
||||||
driver_version="mock-driver-version",
|
|
||||||
server_version="mock-server-version",
|
|
||||||
home_id=1234,
|
|
||||||
min_schema_version=0,
|
|
||||||
max_schema_version=1,
|
|
||||||
)
|
|
||||||
with (
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.zwave_js.config_flow.get_server_version",
|
|
||||||
side_effect=server_version_side_effect,
|
|
||||||
return_value=version_info,
|
|
||||||
) as mock_version,
|
|
||||||
patch(
|
|
||||||
"homeassistant.components.zwave_js.config_flow.SERVER_VERSION_TIMEOUT",
|
|
||||||
new=server_version_timeout,
|
|
||||||
),
|
|
||||||
):
|
|
||||||
yield mock_version
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="server_version_timeout")
|
|
||||||
def mock_server_version_timeout() -> int:
|
|
||||||
"""Patch the timeout for getting server version."""
|
|
||||||
return SERVER_VERSION_TIMEOUT
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="addon_setup_time", autouse=True)
|
@pytest.fixture(name="addon_setup_time", autouse=True)
|
||||||
def mock_addon_setup_time() -> Generator[None]:
|
def mock_addon_setup_time() -> Generator[None]:
|
||||||
"""Mock add-on setup sleep time."""
|
"""Mock add-on setup sleep time."""
|
||||||
|
@ -12,6 +12,7 @@ from homeassistant.components.zwave_js.helpers import get_device_id
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
from tests.components.repairs import (
|
from tests.components.repairs import (
|
||||||
async_process_repairs_platforms,
|
async_process_repairs_platforms,
|
||||||
process_repair_fix_flow,
|
process_repair_fix_flow,
|
||||||
@ -268,3 +269,118 @@ async def test_abort_confirm(
|
|||||||
assert data["type"] == "abort"
|
assert data["type"] == "abort"
|
||||||
assert data["reason"] == "cannot_connect"
|
assert data["reason"] == "cannot_connect"
|
||||||
assert data["description_placeholders"] == {"device_name": device.name}
|
assert data["description_placeholders"] == {"device_name": device.name}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("client")
|
||||||
|
async def test_migrate_unique_id(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: ClientSessionGenerator,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test the migrate unique id flow."""
|
||||||
|
old_unique_id = "123456789"
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="Z-Wave JS",
|
||||||
|
data={
|
||||||
|
"url": "ws://test.org",
|
||||||
|
},
|
||||||
|
unique_id=old_unique_id,
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
|
||||||
|
await async_process_repairs_platforms(hass)
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
http_client = await hass_client()
|
||||||
|
|
||||||
|
# Assert the issue is present
|
||||||
|
await ws_client.send_json_auto_id({"type": "repairs/list_issues"})
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
assert len(msg["result"]["issues"]) == 1
|
||||||
|
issue = msg["result"]["issues"][0]
|
||||||
|
issue_id = issue["issue_id"]
|
||||||
|
assert issue_id == f"migrate_unique_id.{config_entry.entry_id}"
|
||||||
|
|
||||||
|
data = await start_repair_fix_flow(http_client, DOMAIN, issue_id)
|
||||||
|
|
||||||
|
flow_id = data["flow_id"]
|
||||||
|
assert data["step_id"] == "confirm"
|
||||||
|
assert data["description_placeholders"] == {
|
||||||
|
"config_entry_title": "Z-Wave JS",
|
||||||
|
"controller_model": "ZW090",
|
||||||
|
"new_unique_id": "3245146787",
|
||||||
|
"old_unique_id": old_unique_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply fix
|
||||||
|
data = await process_repair_fix_flow(http_client, flow_id)
|
||||||
|
|
||||||
|
assert data["type"] == "create_entry"
|
||||||
|
assert config_entry.unique_id == "3245146787"
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id({"type": "repairs/list_issues"})
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
assert len(msg["result"]["issues"]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("client")
|
||||||
|
async def test_migrate_unique_id_missing_config_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: ClientSessionGenerator,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test the migrate unique id flow with missing config entry."""
|
||||||
|
old_unique_id = "123456789"
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="Z-Wave JS",
|
||||||
|
data={
|
||||||
|
"url": "ws://test.org",
|
||||||
|
},
|
||||||
|
unique_id=old_unique_id,
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
|
||||||
|
await async_process_repairs_platforms(hass)
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
http_client = await hass_client()
|
||||||
|
|
||||||
|
# Assert the issue is present
|
||||||
|
await ws_client.send_json_auto_id({"type": "repairs/list_issues"})
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
assert len(msg["result"]["issues"]) == 1
|
||||||
|
issue = msg["result"]["issues"][0]
|
||||||
|
issue_id = issue["issue_id"]
|
||||||
|
assert issue_id == f"migrate_unique_id.{config_entry.entry_id}"
|
||||||
|
|
||||||
|
await hass.config_entries.async_remove(config_entry.entry_id)
|
||||||
|
|
||||||
|
assert not hass.config_entries.async_get_entry(config_entry.entry_id)
|
||||||
|
|
||||||
|
data = await start_repair_fix_flow(http_client, DOMAIN, issue_id)
|
||||||
|
|
||||||
|
flow_id = data["flow_id"]
|
||||||
|
assert data["step_id"] == "confirm"
|
||||||
|
assert data["description_placeholders"] == {
|
||||||
|
"config_entry_title": "Z-Wave JS",
|
||||||
|
"controller_model": "ZW090",
|
||||||
|
"new_unique_id": "3245146787",
|
||||||
|
"old_unique_id": old_unique_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply fix
|
||||||
|
data = await process_repair_fix_flow(http_client, flow_id)
|
||||||
|
|
||||||
|
assert data["type"] == "create_entry"
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id({"type": "repairs/list_issues"})
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
assert len(msg["result"]["issues"]) == 0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user