mirror of
https://github.com/home-assistant/core.git
synced 2026-04-28 18:12:37 +02:00
Compare commits
53 Commits
2024.3.0b3
...
2024.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 1aa5a07501 | |||
| efe9938b33 | |||
| 1b64989909 | |||
| b480b68e3e | |||
| 5294b492fc | |||
| 080fe4cf5f | |||
| 8b2f40390b | |||
| 3b63719fad | |||
| 061ae756ac | |||
| 862bd8ff07 | |||
| 742710443a | |||
| 015aeadf88 | |||
| b8b654a160 | |||
| 3c5b5ca49b | |||
| fb789d95ed | |||
| 2e6906c8d4 | |||
| cc8d44bbd1 | |||
| 0ad56de6fc | |||
| dedd7a5a41 | |||
| 44c961720c | |||
| 79b1d6df1b | |||
| 274ab2328e | |||
| 93ee900cb3 | |||
| 62474967c9 | |||
| 2cdc8d5f69 | |||
| 4863c94824 | |||
| 193332da74 | |||
| 9926296d35 | |||
| bb6f8b9d57 | |||
| 780f6e8974 | |||
| ab30d44184 | |||
| e23f737fa7 | |||
| b8e3bb8eb8 | |||
| 12574bca8b | |||
| f16ea54b4f | |||
| ad52bf608f | |||
| 46ee52f4ef | |||
| 88fb44bbba | |||
| de5e626430 | |||
| 1bcdba1b4b | |||
| a4353cf39d | |||
| 63192f2291 | |||
| 675b7ca7ba | |||
| df5eb552a0 | |||
| 5017f4a2c7 | |||
| 92d3dccb94 | |||
| 2c38b5ee7b | |||
| 435bb50d29 | |||
| 005493bb5a | |||
| 838a4e4f7b | |||
| bc47c80bbf | |||
| aabaa30fa7 | |||
| 1ee39275fc |
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.7.5"]
|
||||
"requirements": ["aioairzone==0.7.6"]
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
options = ConnectionOptions(
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
True,
|
||||
)
|
||||
|
||||
airzone = AirzoneCloudApi(aiohttp_client.async_get_clientsession(hass), options)
|
||||
|
||||
@@ -94,6 +94,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
ConnectionOptions(
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.3.8"]
|
||||
"requirements": ["aioairzone-cloud==0.4.5"]
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["axis==49"],
|
||||
"requirements": ["axis==50"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
|
||||
from bring_api.bring import Bring
|
||||
from bring_api.exceptions import BringParseException, BringRequestException
|
||||
from bring_api.types import BringItemsResponse, BringList
|
||||
from bring_api.types import BringList, BringPurchase
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -20,8 +20,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class BringData(BringList):
|
||||
"""Coordinator data class."""
|
||||
|
||||
purchase_items: list[BringItemsResponse]
|
||||
recently_items: list[BringItemsResponse]
|
||||
purchase_items: list[BringPurchase]
|
||||
recently_items: list[BringPurchase]
|
||||
|
||||
|
||||
class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bring",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["bring-api==0.4.1"]
|
||||
"requirements": ["bring-api==0.5.5"]
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
import uuid
|
||||
|
||||
from bring_api.exceptions import BringRequestException
|
||||
from bring_api.types import BringItem, BringItemOperation
|
||||
|
||||
from homeassistant.components.todo import (
|
||||
TodoItem,
|
||||
@@ -76,7 +78,7 @@ class BringTodoListEntity(
|
||||
return [
|
||||
*(
|
||||
TodoItem(
|
||||
uid=item["itemId"],
|
||||
uid=item["uuid"],
|
||||
summary=item["itemId"],
|
||||
description=item["specification"] or "",
|
||||
status=TodoItemStatus.NEEDS_ACTION,
|
||||
@@ -85,7 +87,7 @@ class BringTodoListEntity(
|
||||
),
|
||||
*(
|
||||
TodoItem(
|
||||
uid=item["itemId"],
|
||||
uid=item["uuid"],
|
||||
summary=item["itemId"],
|
||||
description=item["specification"] or "",
|
||||
status=TodoItemStatus.COMPLETED,
|
||||
@@ -103,7 +105,10 @@ class BringTodoListEntity(
|
||||
"""Add an item to the To-do list."""
|
||||
try:
|
||||
await self.coordinator.bring.save_item(
|
||||
self.bring_list["listUuid"], item.summary, item.description or ""
|
||||
self.bring_list["listUuid"],
|
||||
item.summary,
|
||||
item.description or "",
|
||||
str(uuid.uuid4()),
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to save todo item for bring") from e
|
||||
@@ -121,60 +126,69 @@ class BringTodoListEntity(
|
||||
|
||||
- Completed items will move to the "completed" section in home assistant todo
|
||||
list and get moved to the recently list in bring
|
||||
- Bring items do not have unique identifiers and are using the
|
||||
name/summery/title. Therefore the name is not to be changed! Should a name
|
||||
be changed anyway, a new item will be created instead and no update for
|
||||
this item is performed and on the next cloud pull update, it will get
|
||||
cleared and replaced seamlessly
|
||||
- Bring shows some odd behaviour when renaming items. This is because Bring
|
||||
did not have unique identifiers for items in the past and this is still
|
||||
a relic from it. Therefore the name is not to be changed! Should a name
|
||||
be changed anyway, the item will be deleted and a new item will be created
|
||||
instead and no update for this item is performed and on the next cloud pull
|
||||
update, it will get cleared and replaced seamlessly.
|
||||
"""
|
||||
|
||||
bring_list = self.bring_list
|
||||
|
||||
bring_purchase_item = next(
|
||||
(i for i in bring_list["purchase_items"] if i["itemId"] == item.uid),
|
||||
(i for i in bring_list["purchase_items"] if i["uuid"] == item.uid),
|
||||
None,
|
||||
)
|
||||
|
||||
bring_recently_item = next(
|
||||
(i for i in bring_list["recently_items"] if i["itemId"] == item.uid),
|
||||
(i for i in bring_list["recently_items"] if i["uuid"] == item.uid),
|
||||
None,
|
||||
)
|
||||
|
||||
current_item = bring_purchase_item or bring_recently_item
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert item.uid
|
||||
assert current_item
|
||||
|
||||
if item.status == TodoItemStatus.COMPLETED and bring_purchase_item:
|
||||
await self.coordinator.bring.complete_item(
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
)
|
||||
|
||||
elif item.status == TodoItemStatus.NEEDS_ACTION and bring_recently_item:
|
||||
await self.coordinator.bring.save_item(
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
)
|
||||
|
||||
elif item.summary == item.uid:
|
||||
if item.summary == current_item["itemId"]:
|
||||
try:
|
||||
await self.coordinator.bring.update_item(
|
||||
await self.coordinator.bring.batch_update_list(
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
item.description or "",
|
||||
BringItem(
|
||||
itemId=item.summary,
|
||||
spec=item.description,
|
||||
uuid=item.uid,
|
||||
),
|
||||
BringItemOperation.ADD
|
||||
if item.status == TodoItemStatus.NEEDS_ACTION
|
||||
else BringItemOperation.COMPLETE,
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to update todo item for bring") from e
|
||||
else:
|
||||
try:
|
||||
await self.coordinator.bring.remove_item(
|
||||
await self.coordinator.bring.batch_update_list(
|
||||
bring_list["listUuid"],
|
||||
item.uid,
|
||||
)
|
||||
await self.coordinator.bring.save_tem(
|
||||
bring_list["listUuid"],
|
||||
item.summary,
|
||||
item.description or "",
|
||||
[
|
||||
BringItem(
|
||||
itemId=current_item["itemId"],
|
||||
spec=item.description,
|
||||
uuid=item.uid,
|
||||
operation=BringItemOperation.REMOVE,
|
||||
),
|
||||
BringItem(
|
||||
itemId=item.summary,
|
||||
spec=item.description,
|
||||
uuid=str(uuid.uuid4()),
|
||||
operation=BringItemOperation.ADD
|
||||
if item.status == TodoItemStatus.NEEDS_ACTION
|
||||
else BringItemOperation.COMPLETE,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to replace todo item for bring") from e
|
||||
|
||||
@@ -182,12 +196,21 @@ class BringTodoListEntity(
|
||||
|
||||
async def async_delete_todo_items(self, uids: list[str]) -> None:
|
||||
"""Delete an item from the To-do list."""
|
||||
for uid in uids:
|
||||
try:
|
||||
await self.coordinator.bring.remove_item(
|
||||
self.bring_list["listUuid"], uid
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to delete todo item for bring") from e
|
||||
|
||||
try:
|
||||
await self.coordinator.bring.batch_update_list(
|
||||
self.bring_list["listUuid"],
|
||||
[
|
||||
BringItem(
|
||||
itemId=uid,
|
||||
spec="",
|
||||
uuid=uid,
|
||||
)
|
||||
for uid in uids
|
||||
],
|
||||
BringItemOperation.REMOVE,
|
||||
)
|
||||
except BringRequestException as e:
|
||||
raise HomeAssistantError("Unable to delete todo item for bring") from e
|
||||
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Config flow for BTHome Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
@@ -11,7 +12,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import onboarding
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfo,
|
||||
BluetoothServiceInfoBleak,
|
||||
async_discovered_service_info,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
@@ -26,11 +27,11 @@ class Discovery:
|
||||
"""A discovered bluetooth device."""
|
||||
|
||||
title: str
|
||||
discovery_info: BluetoothServiceInfo
|
||||
discovery_info: BluetoothServiceInfoBleak
|
||||
device: DeviceData
|
||||
|
||||
|
||||
def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str:
|
||||
def _title(discovery_info: BluetoothServiceInfoBleak, device: DeviceData) -> str:
|
||||
return device.title or device.get_device_name() or discovery_info.name
|
||||
|
||||
|
||||
@@ -41,12 +42,12 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._discovery_info: BluetoothServiceInfo | None = None
|
||||
self._discovery_info: BluetoothServiceInfoBleak | None = None
|
||||
self._discovered_device: DeviceData | None = None
|
||||
self._discovered_devices: dict[str, Discovery] = {}
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfo
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
) -> FlowResult:
|
||||
"""Handle the bluetooth discovery step."""
|
||||
await self.async_set_unique_id(discovery_info.address)
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==3.5.0"]
|
||||
"requirements": ["bthome-ble==3.6.0"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Library for working with CalDAV api."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import caldav
|
||||
|
||||
@@ -13,20 +12,13 @@ async def async_get_calendars(
|
||||
"""Get all calendars that support the specified component."""
|
||||
|
||||
def _get_calendars() -> list[caldav.Calendar]:
|
||||
return client.principal().calendars()
|
||||
|
||||
calendars = await hass.async_add_executor_job(_get_calendars)
|
||||
components_results = await asyncio.gather(
|
||||
*[
|
||||
hass.async_add_executor_job(calendar.get_supported_components)
|
||||
for calendar in calendars
|
||||
return [
|
||||
calendar
|
||||
for calendar in client.principal().calendars()
|
||||
if component in calendar.get_supported_components()
|
||||
]
|
||||
)
|
||||
return [
|
||||
calendar
|
||||
for calendar, supported_components in zip(calendars, components_results)
|
||||
if component in supported_components
|
||||
]
|
||||
|
||||
return await hass.async_add_executor_job(_get_calendars)
|
||||
|
||||
|
||||
def get_attr_value(obj: caldav.CalendarObjectResource, attribute: str) -> str | None:
|
||||
|
||||
@@ -391,6 +391,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
|
||||
prefs = CameraPreferences(hass)
|
||||
await prefs.async_load()
|
||||
hass.data[DATA_CAMERA_PREFS] = prefs
|
||||
|
||||
hass.http.register_view(CameraImageView(component))
|
||||
|
||||
@@ -29,6 +29,8 @@ class DynamicStreamSettings:
|
||||
class CameraPreferences:
|
||||
"""Handle camera preferences."""
|
||||
|
||||
_preload_prefs: dict[str, dict[str, bool | Orientation]]
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize camera prefs."""
|
||||
self._hass = hass
|
||||
@@ -41,6 +43,10 @@ class CameraPreferences:
|
||||
str, DynamicStreamSettings
|
||||
] = {}
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Initialize the camera preferences."""
|
||||
self._preload_prefs = await self._store.async_load() or {}
|
||||
|
||||
async def async_update(
|
||||
self,
|
||||
entity_id: str,
|
||||
@@ -63,9 +69,8 @@ class CameraPreferences:
|
||||
if preload_stream is not UNDEFINED:
|
||||
if dynamic_stream_settings:
|
||||
dynamic_stream_settings.preload_stream = preload_stream
|
||||
preload_prefs = await self._store.async_load() or {}
|
||||
preload_prefs[entity_id] = {PREF_PRELOAD_STREAM: preload_stream}
|
||||
await self._store.async_save(preload_prefs)
|
||||
self._preload_prefs[entity_id] = {PREF_PRELOAD_STREAM: preload_stream}
|
||||
await self._store.async_save(self._preload_prefs)
|
||||
|
||||
if orientation is not UNDEFINED:
|
||||
if (registry := er.async_get(self._hass)).async_get(entity_id):
|
||||
@@ -91,10 +96,10 @@ class CameraPreferences:
|
||||
# Get orientation setting from entity registry
|
||||
reg_entry = er.async_get(self._hass).async_get(entity_id)
|
||||
er_prefs: Mapping = reg_entry.options.get(DOMAIN, {}) if reg_entry else {}
|
||||
preload_prefs = await self._store.async_load() or {}
|
||||
settings = DynamicStreamSettings(
|
||||
preload_stream=cast(
|
||||
bool, preload_prefs.get(entity_id, {}).get(PREF_PRELOAD_STREAM, False)
|
||||
bool,
|
||||
self._preload_prefs.get(entity_id, {}).get(PREF_PRELOAD_STREAM, False),
|
||||
),
|
||||
orientation=er_prefs.get(PREF_ORIENTATION, Orientation.NO_TRANSFORM),
|
||||
)
|
||||
|
||||
@@ -165,6 +165,7 @@ class DeconzBaseLight(DeconzDevice[_LightDeviceT], LightEntity):
|
||||
"""Representation of a deCONZ light."""
|
||||
|
||||
TYPE = DOMAIN
|
||||
_attr_color_mode = ColorMode.UNKNOWN
|
||||
|
||||
def __init__(self, device: _LightDeviceT, gateway: DeconzGateway) -> None:
|
||||
"""Set up light."""
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240228.1"]
|
||||
"requirements": ["home-assistant-frontend==20240306.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.43", "babel==2.13.1"]
|
||||
"requirements": ["holidays==0.44", "babel==2.13.1"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@ import contextlib
|
||||
import logging
|
||||
|
||||
import aiohomekit
|
||||
from aiohomekit.const import (
|
||||
BLE_TRANSPORT_SUPPORTED,
|
||||
COAP_TRANSPORT_SUPPORTED,
|
||||
IP_TRANSPORT_SUPPORTED,
|
||||
)
|
||||
from aiohomekit.exceptions import (
|
||||
AccessoryDisconnectedError,
|
||||
AccessoryNotFoundError,
|
||||
@@ -24,6 +29,15 @@ from .connection import HKDevice
|
||||
from .const import DOMAIN, KNOWN_DEVICES
|
||||
from .utils import async_get_controller
|
||||
|
||||
# Ensure all the controllers get imported in the executor
|
||||
# since they are loaded late.
|
||||
if BLE_TRANSPORT_SUPPORTED:
|
||||
from aiohomekit.controller import ble # noqa: F401
|
||||
if COAP_TRANSPORT_SUPPORTED:
|
||||
from aiohomekit.controller import coap # noqa: F401
|
||||
if IP_TRANSPORT_SUPPORTED:
|
||||
from aiohomekit.controller import ip # noqa: F401
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
@@ -6,7 +6,7 @@ from aioautomower.session import AutomowerSession
|
||||
from aiohttp import ClientError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||
@@ -17,7 +17,6 @@ from .coordinator import AutomowerDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.LAWN_MOWER, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
@@ -38,13 +37,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await api_api.async_get_access_token()
|
||||
except ClientError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
coordinator = AutomowerDataUpdateCoordinator(hass, automower_api)
|
||||
coordinator = AutomowerDataUpdateCoordinator(hass, automower_api, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.async_create_background_task(
|
||||
hass,
|
||||
coordinator.client_listen(hass, entry, automower_api),
|
||||
"websocket_task",
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
@@ -52,8 +53,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Handle unload of an entry."""
|
||||
coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
await coordinator.shutdown()
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
"""Data UpdateCoordinator for the Husqvarna Automower integration."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioautomower.exceptions import ApiException, HusqvarnaWSServerHandshakeError
|
||||
from aioautomower.model import MowerAttributes
|
||||
from aioautomower.session import AutomowerSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .api import AsyncConfigEntryAuth
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
MAX_WS_RECONNECT_TIME = 600
|
||||
|
||||
|
||||
class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]):
|
||||
"""Class to manage fetching Husqvarna data."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, api: AsyncConfigEntryAuth) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, api: AutomowerSession, entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Initialize data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
@@ -35,13 +40,39 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
|
||||
await self.api.connect()
|
||||
self.api.register_data_callback(self.callback)
|
||||
self.ws_connected = True
|
||||
return await self.api.get_status()
|
||||
|
||||
async def shutdown(self, *_: Any) -> None:
|
||||
"""Close resources."""
|
||||
await self.api.close()
|
||||
try:
|
||||
return await self.api.get_status()
|
||||
except ApiException as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
||||
@callback
|
||||
def callback(self, ws_data: dict[str, MowerAttributes]) -> None:
|
||||
"""Process websocket callbacks and write them to the DataUpdateCoordinator."""
|
||||
self.async_set_updated_data(ws_data)
|
||||
|
||||
async def client_listen(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
automower_client: AutomowerSession,
|
||||
reconnect_time: int = 2,
|
||||
) -> None:
|
||||
"""Listen with the client."""
|
||||
try:
|
||||
await automower_client.auth.websocket_connect()
|
||||
reconnect_time = 2
|
||||
await automower_client.start_listening()
|
||||
except HusqvarnaWSServerHandshakeError as err:
|
||||
_LOGGER.debug(
|
||||
"Failed to connect to websocket. Trying to reconnect: %s", err
|
||||
)
|
||||
|
||||
if not hass.is_stopping:
|
||||
await asyncio.sleep(reconnect_time)
|
||||
reconnect_time = min(reconnect_time * 2, MAX_WS_RECONNECT_TIME)
|
||||
await self.client_listen(
|
||||
hass=hass,
|
||||
entry=entry,
|
||||
automower_client=automower_client,
|
||||
reconnect_time=reconnect_time,
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
|
||||
"iot_class": "cloud_push",
|
||||
"requirements": ["aioautomower==2024.2.7"]
|
||||
"requirements": ["aioautomower==2024.2.10"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2024.2.0"]
|
||||
"requirements": ["pydrawise==2024.3.0"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"loggers": ["xknx", "xknxproject"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"xknx==2.12.1",
|
||||
"xknx==2.12.2",
|
||||
"xknxproject==3.7.0",
|
||||
"knx-frontend==2024.1.20.105944"
|
||||
]
|
||||
|
||||
@@ -148,7 +148,10 @@ async def async_resolve_media(
|
||||
raise Unresolvable("Media Source not loaded")
|
||||
|
||||
if target_media_player is UNDEFINED:
|
||||
report("calls media_source.async_resolve_media without passing an entity_id")
|
||||
report(
|
||||
"calls media_source.async_resolve_media without passing an entity_id",
|
||||
{DOMAIN},
|
||||
)
|
||||
target_media_player = None
|
||||
|
||||
try:
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
@@ -41,9 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
try:
|
||||
await api.async_initialize()
|
||||
except MinecraftServerAddressError as error:
|
||||
raise ConfigEntryError(
|
||||
f"Server address in configuration entry is invalid: {error}"
|
||||
) from error
|
||||
raise ConfigEntryNotReady(f"Initialization failed: {error}") from error
|
||||
|
||||
# Create coordinator instance.
|
||||
coordinator = MinecraftServerCoordinator(hass, entry.data[CONF_NAME], api)
|
||||
|
||||
@@ -308,7 +308,7 @@ def check_config(config: dict) -> dict:
|
||||
) -> bool:
|
||||
"""Validate entity."""
|
||||
name = entity[CONF_NAME]
|
||||
addr = str(entity[CONF_ADDRESS])
|
||||
addr = f"{hub_name}{entity[CONF_ADDRESS]}"
|
||||
scan_interval = entity.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
if scan_interval < 5:
|
||||
_LOGGER.warning(
|
||||
@@ -335,11 +335,15 @@ def check_config(config: dict) -> dict:
|
||||
loc_addr: set[str] = {addr}
|
||||
|
||||
if CONF_TARGET_TEMP in entity:
|
||||
loc_addr.add(f"{entity[CONF_TARGET_TEMP]}_{inx}")
|
||||
loc_addr.add(f"{hub_name}{entity[CONF_TARGET_TEMP]}_{inx}")
|
||||
if CONF_HVAC_MODE_REGISTER in entity:
|
||||
loc_addr.add(f"{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}")
|
||||
loc_addr.add(
|
||||
f"{hub_name}{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}"
|
||||
)
|
||||
if CONF_FAN_MODE_REGISTER in entity:
|
||||
loc_addr.add(f"{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}")
|
||||
loc_addr.add(
|
||||
f"{hub_name}{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}"
|
||||
)
|
||||
|
||||
dup_addrs = ent_addr.intersection(loc_addr)
|
||||
if len(dup_addrs) > 0:
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.13.7"],
|
||||
"requirements": ["pyoverkiz==1.13.8"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_kizbox._tcp.local.",
|
||||
|
||||
@@ -177,6 +177,7 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = (
|
||||
key="record_distance",
|
||||
translation_key="record_distance",
|
||||
icon="mdi:map-marker-distance",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -11,11 +11,10 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
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 format_mac
|
||||
|
||||
from .const import CONF_SERIAL_NUMBER
|
||||
from .coordinator import RainbirdData
|
||||
from .coordinator import RainbirdData, async_create_clientsession
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,9 +35,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
clientsession = async_create_clientsession()
|
||||
entry.async_on_unload(clientsession.close)
|
||||
controller = AsyncRainbirdController(
|
||||
AsyncRainbirdClient(
|
||||
async_get_clientsession(hass),
|
||||
clientsession,
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
@@ -20,7 +20,6 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import config_validation as cv, selector
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import (
|
||||
@@ -30,6 +29,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
TIMEOUT_SECONDS,
|
||||
)
|
||||
from .coordinator import async_create_clientsession
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -101,9 +101,10 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
Raises a ConfigFlowError on failure.
|
||||
"""
|
||||
clientsession = async_create_clientsession()
|
||||
controller = AsyncRainbirdController(
|
||||
AsyncRainbirdClient(
|
||||
async_get_clientsession(self.hass),
|
||||
clientsession,
|
||||
host,
|
||||
password,
|
||||
)
|
||||
@@ -124,6 +125,8 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
f"Error connecting to Rain Bird controller: {str(err)}",
|
||||
"cannot_connect",
|
||||
) from err
|
||||
finally:
|
||||
await clientsession.close()
|
||||
|
||||
async def async_finish(
|
||||
self,
|
||||
|
||||
@@ -9,6 +9,7 @@ from functools import cached_property
|
||||
import logging
|
||||
from typing import TypeVar
|
||||
|
||||
import aiohttp
|
||||
from pyrainbird.async_client import (
|
||||
AsyncRainbirdController,
|
||||
RainbirdApiException,
|
||||
@@ -18,6 +19,7 @@ from pyrainbird.data import ModelAndVersion, Schedule
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -28,6 +30,13 @@ UPDATE_INTERVAL = datetime.timedelta(minutes=1)
|
||||
# changes, so we refresh it less often.
|
||||
CALENDAR_UPDATE_INTERVAL = datetime.timedelta(minutes=15)
|
||||
|
||||
# The valves state are not immediately reflected after issuing a command. We add
|
||||
# small delay to give additional time to reflect the new state.
|
||||
DEBOUNCER_COOLDOWN = 5
|
||||
|
||||
# Rainbird devices can only accept a single request at a time
|
||||
CONECTION_LIMIT = 1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
@@ -43,6 +52,13 @@ class RainbirdDeviceState:
|
||||
rain_delay: int
|
||||
|
||||
|
||||
def async_create_clientsession() -> aiohttp.ClientSession:
|
||||
"""Create a rainbird async_create_clientsession with a connection limit."""
|
||||
return aiohttp.ClientSession(
|
||||
connector=aiohttp.TCPConnector(limit=CONECTION_LIMIT),
|
||||
)
|
||||
|
||||
|
||||
class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
|
||||
"""Coordinator for rainbird API calls."""
|
||||
|
||||
@@ -60,6 +76,9 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
|
||||
_LOGGER,
|
||||
name=name,
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
request_refresh_debouncer=Debouncer(
|
||||
hass, _LOGGER, cooldown=DEBOUNCER_COOLDOWN, immediate=False
|
||||
),
|
||||
)
|
||||
self._controller = controller
|
||||
self._unique_id = unique_id
|
||||
|
||||
@@ -103,6 +103,10 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity)
|
||||
except RainbirdApiException as err:
|
||||
raise HomeAssistantError("Rain Bird device failure") from err
|
||||
|
||||
# The device reflects the old state for a few moments. Update the
|
||||
# state manually and trigger a refresh after a short debounced delay.
|
||||
self.coordinator.data.active_zones.add(self._zone)
|
||||
self.async_write_ha_state()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
@@ -115,6 +119,11 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity)
|
||||
) from err
|
||||
except RainbirdApiException as err:
|
||||
raise HomeAssistantError("Rain Bird device failure") from err
|
||||
|
||||
# The device reflects the old state for a few moments. Update the
|
||||
# state manually and trigger a refresh after a short debounced delay.
|
||||
self.coordinator.data.active_zones.remove(self._zone)
|
||||
self.async_write_ha_state()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"title": "Random sensor"
|
||||
},
|
||||
"user": {
|
||||
"description": "This helper allow you to create a helper that emits a random value.",
|
||||
"description": "This helper allows you to create a helper that emits a random value.",
|
||||
"menu_options": {
|
||||
"binary_sensor": "Random binary sensor",
|
||||
"sensor": "Random sensor"
|
||||
|
||||
@@ -18,5 +18,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"requirements": ["reolink-aio==0.8.8"]
|
||||
"requirements": ["reolink-aio==0.8.9"]
|
||||
}
|
||||
|
||||
@@ -367,7 +367,8 @@
|
||||
"state": {
|
||||
"stayoff": "Stay off",
|
||||
"auto": "Auto",
|
||||
"alwaysonatnight": "Auto & always on at night"
|
||||
"alwaysonatnight": "Auto & always on at night",
|
||||
"alwayson": "Always on"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["roborock"],
|
||||
"requirements": [
|
||||
"python-roborock==0.39.2",
|
||||
"python-roborock==0.40.0",
|
||||
"vacuum-map-parser-roborock==0.1.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -62,8 +62,11 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
station = await self._tankerkoenig.station_details(station_id)
|
||||
except TankerkoenigInvalidKeyError as err:
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
except (TankerkoenigError, TankerkoenigConnectionError) as err:
|
||||
except TankerkoenigConnectionError as err:
|
||||
raise ConfigEntryNotReady(err) from err
|
||||
except TankerkoenigError as err:
|
||||
_LOGGER.error("Error when adding station %s %s", station_id, err)
|
||||
continue
|
||||
|
||||
self.stations[station_id] = station
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/tedee",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["pytedee-async==0.2.13"]
|
||||
"requirements": ["pytedee-async==0.2.15"]
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"title": "Template sensor"
|
||||
},
|
||||
"user": {
|
||||
"description": "This helper allow you to create helper entities that define their state using a template.",
|
||||
"description": "This helper allows you to create helper entities that define their state using a template.",
|
||||
"menu_options": {
|
||||
"binary_sensor": "Template a binary sensor",
|
||||
"sensor": "Template a sensor"
|
||||
|
||||
@@ -60,6 +60,35 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
RT_SENSORS_UNIQUE_ID_MIGRATION = {
|
||||
"accumulated_consumption_last_hour": "accumulated consumption current hour",
|
||||
"accumulated_production_last_hour": "accumulated production current hour",
|
||||
"current_l1": "current L1",
|
||||
"current_l2": "current L2",
|
||||
"current_l3": "current L3",
|
||||
"estimated_hour_consumption": "Estimated consumption current hour",
|
||||
}
|
||||
|
||||
RT_SENSORS_UNIQUE_ID_MIGRATION_SIMPLE = {
|
||||
# simple migration can be done by replacing " " with "_"
|
||||
"accumulated_consumption",
|
||||
"accumulated_cost",
|
||||
"accumulated_production",
|
||||
"accumulated_reward",
|
||||
"average_power",
|
||||
"last_meter_consumption",
|
||||
"last_meter_production",
|
||||
"max_power",
|
||||
"min_power",
|
||||
"power_factor",
|
||||
"power_production",
|
||||
"signal_strength",
|
||||
"voltage_phase1",
|
||||
"voltage_phase2",
|
||||
"voltage_phase3",
|
||||
}
|
||||
|
||||
|
||||
RT_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="averagePower",
|
||||
@@ -454,7 +483,7 @@ class TibberSensorRT(TibberSensor, CoordinatorEntity["TibberRtDataCoordinator"])
|
||||
self._device_name = f"{self._model} {self._home_name}"
|
||||
|
||||
self._attr_native_value = initial_state
|
||||
self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.name}"
|
||||
self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.key}"
|
||||
|
||||
if description.key in ("accumulatedCost", "accumulatedReward"):
|
||||
self._attr_native_unit_of_measurement = tibber_home.currency
|
||||
@@ -523,6 +552,7 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en
|
||||
self._async_remove_device_updates_handler = self.async_add_listener(
|
||||
self._add_sensors
|
||||
)
|
||||
self.entity_registry = async_get_entity_reg(hass)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
|
||||
|
||||
@callback
|
||||
@@ -530,6 +560,49 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en
|
||||
"""Handle Home Assistant stopping."""
|
||||
self._async_remove_device_updates_handler()
|
||||
|
||||
@callback
|
||||
def _migrate_unique_id(self, sensor_description: SensorEntityDescription) -> None:
|
||||
"""Migrate unique id if needed."""
|
||||
home_id = self._tibber_home.home_id
|
||||
translation_key = sensor_description.translation_key
|
||||
description_key = sensor_description.key
|
||||
entity_id: str | None = None
|
||||
if translation_key in RT_SENSORS_UNIQUE_ID_MIGRATION_SIMPLE:
|
||||
entity_id = self.entity_registry.async_get_entity_id(
|
||||
"sensor",
|
||||
TIBBER_DOMAIN,
|
||||
f"{home_id}_rt_{translation_key.replace('_', ' ')}",
|
||||
)
|
||||
elif translation_key in RT_SENSORS_UNIQUE_ID_MIGRATION:
|
||||
entity_id = self.entity_registry.async_get_entity_id(
|
||||
"sensor",
|
||||
TIBBER_DOMAIN,
|
||||
f"{home_id}_rt_{RT_SENSORS_UNIQUE_ID_MIGRATION[translation_key]}",
|
||||
)
|
||||
elif translation_key != description_key:
|
||||
entity_id = self.entity_registry.async_get_entity_id(
|
||||
"sensor",
|
||||
TIBBER_DOMAIN,
|
||||
f"{home_id}_rt_{translation_key}",
|
||||
)
|
||||
|
||||
if entity_id is None:
|
||||
return
|
||||
|
||||
new_unique_id = f"{home_id}_rt_{description_key}"
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migrating unique id for %s to %s",
|
||||
entity_id,
|
||||
new_unique_id,
|
||||
)
|
||||
try:
|
||||
self.entity_registry.async_update_entity(
|
||||
entity_id, new_unique_id=new_unique_id
|
||||
)
|
||||
except ValueError as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
@callback
|
||||
def _add_sensors(self) -> None:
|
||||
"""Add sensor."""
|
||||
@@ -543,6 +616,8 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en
|
||||
state = live_measurement.get(sensor_description.key)
|
||||
if state is None:
|
||||
continue
|
||||
|
||||
self._migrate_unique_id(sensor_description)
|
||||
entity = TibberSensorRT(
|
||||
self._tibber_home,
|
||||
sensor_description,
|
||||
|
||||
@@ -196,12 +196,10 @@ class UnifiHub:
|
||||
def async_add_unifi_entities() -> None:
|
||||
"""Add UniFi entity."""
|
||||
async_add_entities(
|
||||
[
|
||||
unifi_platform_entity(obj_id, self, description)
|
||||
for description in descriptions
|
||||
for obj_id in description.api_handler_fn(self.api)
|
||||
if self._async_should_add_entity(description, obj_id)
|
||||
]
|
||||
unifi_platform_entity(obj_id, self, description)
|
||||
for description in descriptions
|
||||
for obj_id in description.api_handler_fn(self.api)
|
||||
if self._async_should_add_entity(description, obj_id)
|
||||
)
|
||||
|
||||
async_add_unifi_entities()
|
||||
|
||||
@@ -5,8 +5,14 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp.client_exceptions import ServerDisconnectedError
|
||||
from pyunifiprotect.data import Bootstrap
|
||||
from pyunifiprotect.exceptions import ClientError, NotAuthorized
|
||||
|
||||
# Import the test_util.anonymize module from the pyunifiprotect package
|
||||
# in __init__ to ensure it gets imported in the executor since the
|
||||
# diagnostics module will not be imported in the executor.
|
||||
from pyunifiprotect.test_util.anonymize import anonymize_data # noqa: F401
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -123,7 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
try:
|
||||
await _async_setup_entry(hass, entry, data_service)
|
||||
await _async_setup_entry(hass, entry, data_service, bootstrap)
|
||||
except Exception as err:
|
||||
if await nvr_info.get_is_prerelease():
|
||||
# If they are running a pre-release, its quite common for setup
|
||||
@@ -151,9 +157,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
|
||||
async def _async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, data_service: ProtectData
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
data_service: ProtectData,
|
||||
bootstrap: Bootstrap,
|
||||
) -> None:
|
||||
await async_migrate_data(hass, entry, data_service.api)
|
||||
await async_migrate_data(hass, entry, data_service.api, bootstrap)
|
||||
|
||||
await data_service.async_setup()
|
||||
if not data_service.last_update_success:
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyunifiprotect", "unifi_discovery"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyunifiprotect==4.23.3", "unifi-discovery==1.1.7"],
|
||||
"requirements": ["pyunifiprotect==4.23.3", "unifi-discovery==1.1.8"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -3,47 +3,39 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from aiohttp.client_exceptions import ServerDisconnectedError
|
||||
from pyunifiprotect import ProtectApiClient
|
||||
from pyunifiprotect.data import NVR, Bootstrap, ProtectAdoptableDeviceModel
|
||||
from pyunifiprotect.exceptions import ClientError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_migrate_data(
|
||||
hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
protect: ProtectApiClient,
|
||||
bootstrap: Bootstrap,
|
||||
) -> None:
|
||||
"""Run all valid UniFi Protect data migrations."""
|
||||
|
||||
_LOGGER.debug("Start Migrate: async_migrate_buttons")
|
||||
await async_migrate_buttons(hass, entry, protect)
|
||||
await async_migrate_buttons(hass, entry, protect, bootstrap)
|
||||
_LOGGER.debug("Completed Migrate: async_migrate_buttons")
|
||||
|
||||
_LOGGER.debug("Start Migrate: async_migrate_device_ids")
|
||||
await async_migrate_device_ids(hass, entry, protect)
|
||||
await async_migrate_device_ids(hass, entry, protect, bootstrap)
|
||||
_LOGGER.debug("Completed Migrate: async_migrate_device_ids")
|
||||
|
||||
|
||||
async def async_get_bootstrap(protect: ProtectApiClient) -> Bootstrap:
|
||||
"""Get UniFi Protect bootstrap or raise appropriate HA error."""
|
||||
|
||||
try:
|
||||
bootstrap = await protect.get_bootstrap()
|
||||
except (TimeoutError, ClientError, ServerDisconnectedError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
return bootstrap
|
||||
|
||||
|
||||
async def async_migrate_buttons(
|
||||
hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
protect: ProtectApiClient,
|
||||
bootstrap: Bootstrap,
|
||||
) -> None:
|
||||
"""Migrate existing Reboot button unique IDs from {device_id} to {deivce_id}_reboot.
|
||||
|
||||
@@ -63,7 +55,6 @@ async def async_migrate_buttons(
|
||||
_LOGGER.debug("No button entities need migration")
|
||||
return
|
||||
|
||||
bootstrap = await async_get_bootstrap(protect)
|
||||
count = 0
|
||||
for button in to_migrate:
|
||||
device = bootstrap.get_device_from_id(button.unique_id)
|
||||
@@ -94,7 +85,10 @@ async def async_migrate_buttons(
|
||||
|
||||
|
||||
async def async_migrate_device_ids(
|
||||
hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
protect: ProtectApiClient,
|
||||
bootstrap: Bootstrap,
|
||||
) -> None:
|
||||
"""Migrate unique IDs from {device_id}_{name} format to {mac}_{name} format.
|
||||
|
||||
@@ -119,7 +113,6 @@ async def async_migrate_device_ids(
|
||||
_LOGGER.debug("No entities need migration to MAC address ID")
|
||||
return
|
||||
|
||||
bootstrap = await async_get_bootstrap(protect)
|
||||
count = 0
|
||||
for entity in to_migrate:
|
||||
parts = entity.unique_id.split("_")
|
||||
|
||||
@@ -22,8 +22,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
),
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -205,7 +205,8 @@ class ViCareClimate(ViCareEntity, ClimateEntity):
|
||||
"heating_curve_shift"
|
||||
] = self._circuit.getHeatingCurveShift()
|
||||
|
||||
self._attributes["vicare_modes"] = self._circuit.getModes()
|
||||
with suppress(PyViCareNotSupportedFeatureError):
|
||||
self._attributes["vicare_modes"] = self._circuit.getModes()
|
||||
|
||||
self._current_action = False
|
||||
# Update the specific device attributes
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["weatherflow4py==0.1.11"]
|
||||
"requirements": ["weatherflow4py==0.1.12"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.43"]
|
||||
"requirements": ["holidays==0.44"]
|
||||
}
|
||||
|
||||
@@ -87,6 +87,9 @@ class ZHAGroupMember(LogMixin):
|
||||
entity_info = []
|
||||
|
||||
for entity_ref in zha_device_registry.get(self.device.ieee):
|
||||
# We have device entities now that don't leverage cluster handlers
|
||||
if not entity_ref.cluster_handlers:
|
||||
continue
|
||||
entity = entity_registry.async_get(entity_ref.reference_id)
|
||||
handler = list(entity_ref.cluster_handlers.values())[0]
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.112",
|
||||
"zigpy-deconz==0.23.1",
|
||||
"zigpy==0.63.2",
|
||||
"zigpy==0.63.4",
|
||||
"zigpy-xbee==0.20.1",
|
||||
"zigpy-zigate==0.12.0",
|
||||
"zigpy-znp==0.12.1",
|
||||
|
||||
@@ -16,7 +16,7 @@ from .helpers.deprecation import (
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 3
|
||||
PATCH_VERSION: Final = "0b3"
|
||||
PATCH_VERSION: Final = "0"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
||||
|
||||
@@ -10,7 +10,6 @@ from datetime import (
|
||||
timedelta,
|
||||
)
|
||||
from enum import Enum, StrEnum
|
||||
import inspect
|
||||
import logging
|
||||
from numbers import Number
|
||||
import os
|
||||
@@ -103,6 +102,7 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.yaml.objects import NodeStrClass
|
||||
|
||||
from . import script_variables as script_variables_helper, template as template_helper
|
||||
from .frame import get_integration_logger
|
||||
|
||||
TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM', 'HH:MM:SS' or 'HH:MM:SS.F'"
|
||||
|
||||
@@ -890,24 +890,17 @@ def _deprecated_or_removed(
|
||||
- No warning if neither key nor replacement_key are provided
|
||||
- Adds replacement_key with default value in this case
|
||||
"""
|
||||
module = inspect.getmodule(inspect.stack(context=0)[2].frame)
|
||||
if module is not None:
|
||||
module_name = module.__name__
|
||||
else:
|
||||
# If Python is unable to access the sources files, the call stack frame
|
||||
# will be missing information, so let's guard.
|
||||
# https://github.com/home-assistant/core/issues/24982
|
||||
module_name = __name__
|
||||
if option_removed:
|
||||
logger_func = logging.getLogger(module_name).error
|
||||
option_status = "has been removed"
|
||||
else:
|
||||
logger_func = logging.getLogger(module_name).warning
|
||||
option_status = "is deprecated"
|
||||
|
||||
def validator(config: dict) -> dict:
|
||||
"""Check if key is in config and log warning or error."""
|
||||
if key in config:
|
||||
if option_removed:
|
||||
level = logging.ERROR
|
||||
option_status = "has been removed"
|
||||
else:
|
||||
level = logging.WARNING
|
||||
option_status = "is deprecated"
|
||||
|
||||
try:
|
||||
near = (
|
||||
f"near {config.__config_file__}" # type: ignore[attr-defined]
|
||||
@@ -928,7 +921,7 @@ def _deprecated_or_removed(
|
||||
if raise_if_present:
|
||||
raise vol.Invalid(warning % arguments)
|
||||
|
||||
logger_func(warning, *arguments)
|
||||
get_integration_logger(__name__).log(level, warning, *arguments)
|
||||
value = config[key]
|
||||
if replacement_key or option_removed:
|
||||
config.pop(key)
|
||||
@@ -1112,19 +1105,9 @@ def expand_condition_shorthand(value: Any | None) -> Any:
|
||||
def empty_config_schema(domain: str) -> Callable[[dict], dict]:
|
||||
"""Return a config schema which logs if there are configuration parameters."""
|
||||
|
||||
module = inspect.getmodule(inspect.stack(context=0)[2].frame)
|
||||
if module is not None:
|
||||
module_name = module.__name__
|
||||
else:
|
||||
# If Python is unable to access the sources files, the call stack frame
|
||||
# will be missing information, so let's guard.
|
||||
# https://github.com/home-assistant/core/issues/24982
|
||||
module_name = __name__
|
||||
logger_func = logging.getLogger(module_name).error
|
||||
|
||||
def validator(config: dict) -> dict:
|
||||
if domain in config and config[domain]:
|
||||
logger_func(
|
||||
get_integration_logger(__name__).error(
|
||||
(
|
||||
"The %s integration does not support any configuration parameters, "
|
||||
"got %s. Please remove the configuration parameters from your "
|
||||
@@ -1146,16 +1129,6 @@ def _no_yaml_config_schema(
|
||||
) -> Callable[[dict], dict]:
|
||||
"""Return a config schema which logs if attempted to setup from YAML."""
|
||||
|
||||
module = inspect.getmodule(inspect.stack(context=0)[2].frame)
|
||||
if module is not None:
|
||||
module_name = module.__name__
|
||||
else:
|
||||
# If Python is unable to access the sources files, the call stack frame
|
||||
# will be missing information, so let's guard.
|
||||
# https://github.com/home-assistant/core/issues/24982
|
||||
module_name = __name__
|
||||
logger_func = logging.getLogger(module_name).error
|
||||
|
||||
def raise_issue() -> None:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .issue_registry import IssueSeverity, async_create_issue
|
||||
@@ -1176,7 +1149,7 @@ def _no_yaml_config_schema(
|
||||
|
||||
def validator(config: dict) -> dict:
|
||||
if domain in config:
|
||||
logger_func(
|
||||
get_integration_logger(__name__).error(
|
||||
(
|
||||
"The %s integration does not support YAML setup, please remove it "
|
||||
"from your configuration file"
|
||||
|
||||
@@ -34,6 +34,26 @@ class IntegrationFrame:
|
||||
relative_filename: str
|
||||
|
||||
|
||||
def get_integration_logger(fallback_name: str) -> logging.Logger:
|
||||
"""Return a logger by checking the current integration frame.
|
||||
|
||||
If Python is unable to access the sources files, the call stack frame
|
||||
will be missing information, so let's guard by requiring a fallback name.
|
||||
https://github.com/home-assistant/core/issues/24982
|
||||
"""
|
||||
try:
|
||||
integration_frame = get_integration_frame()
|
||||
except MissingIntegrationFrame:
|
||||
return logging.getLogger(fallback_name)
|
||||
|
||||
if integration_frame.custom_integration:
|
||||
logger_name = f"custom_components.{integration_frame.integration}"
|
||||
else:
|
||||
logger_name = f"homeassistant.components.{integration_frame.integration}"
|
||||
|
||||
return logging.getLogger(logger_name)
|
||||
|
||||
|
||||
def get_integration_frame(exclude_integrations: set | None = None) -> IntegrationFrame:
|
||||
"""Return the frame, integration and integration path of the current stack frame."""
|
||||
found_frame = None
|
||||
|
||||
+60
-5
@@ -52,6 +52,20 @@ _CallableT = TypeVar("_CallableT", bound=Callable[..., Any])
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlockedIntegration:
|
||||
"""Blocked custom integration details."""
|
||||
|
||||
lowest_good_version: AwesomeVersion | None
|
||||
reason: str
|
||||
|
||||
|
||||
BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = {
|
||||
# Added in 2024.3.0 because of https://github.com/home-assistant/core/issues/112464
|
||||
"start_time": BlockedIntegration(AwesomeVersion("1.1.7"), "breaks Home Assistant")
|
||||
}
|
||||
|
||||
DATA_COMPONENTS = "components"
|
||||
DATA_INTEGRATIONS = "integrations"
|
||||
DATA_MISSING_PLATFORMS = "missing_platforms"
|
||||
@@ -599,6 +613,7 @@ class Integration:
|
||||
return integration
|
||||
|
||||
_LOGGER.warning(CUSTOM_WARNING, integration.domain)
|
||||
|
||||
if integration.version is None:
|
||||
_LOGGER.error(
|
||||
(
|
||||
@@ -635,6 +650,21 @@ class Integration:
|
||||
integration.version,
|
||||
)
|
||||
return None
|
||||
|
||||
if blocked := BLOCKED_CUSTOM_INTEGRATIONS.get(integration.domain):
|
||||
if _version_blocked(integration.version, blocked):
|
||||
_LOGGER.error(
|
||||
(
|
||||
"Version %s of custom integration '%s' %s and was blocked "
|
||||
"from loading, please %s"
|
||||
),
|
||||
integration.version,
|
||||
integration.domain,
|
||||
blocked.reason,
|
||||
async_suggest_report_issue(None, integration=integration),
|
||||
)
|
||||
return None
|
||||
|
||||
return integration
|
||||
|
||||
return None
|
||||
@@ -1032,6 +1062,20 @@ class Integration:
|
||||
return f"<Integration {self.domain}: {self.pkg_path}>"
|
||||
|
||||
|
||||
def _version_blocked(
|
||||
integration_version: AwesomeVersion,
|
||||
blocked_integration: BlockedIntegration,
|
||||
) -> bool:
|
||||
"""Return True if the integration version is blocked."""
|
||||
if blocked_integration.lowest_good_version is None:
|
||||
return True
|
||||
|
||||
if integration_version >= blocked_integration.lowest_good_version:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _resolve_integrations_from_root(
|
||||
hass: HomeAssistant, root_module: ModuleType, domains: Iterable[str]
|
||||
) -> dict[str, Integration]:
|
||||
@@ -1387,6 +1431,7 @@ def is_component_module_loaded(hass: HomeAssistant, module: str) -> bool:
|
||||
def async_get_issue_tracker(
|
||||
hass: HomeAssistant | None,
|
||||
*,
|
||||
integration: Integration | None = None,
|
||||
integration_domain: str | None = None,
|
||||
module: str | None = None,
|
||||
) -> str | None:
|
||||
@@ -1394,19 +1439,23 @@ def async_get_issue_tracker(
|
||||
issue_tracker = (
|
||||
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue"
|
||||
)
|
||||
if not integration_domain and not module:
|
||||
if not integration and not integration_domain and not module:
|
||||
# If we know nothing about the entity, suggest opening an issue on HA core
|
||||
return issue_tracker
|
||||
|
||||
if hass and integration_domain:
|
||||
if not integration and (hass and integration_domain):
|
||||
with suppress(IntegrationNotLoaded):
|
||||
integration = async_get_loaded_integration(hass, integration_domain)
|
||||
if not integration.is_built_in:
|
||||
return integration.issue_tracker
|
||||
|
||||
if integration and not integration.is_built_in:
|
||||
return integration.issue_tracker
|
||||
|
||||
if module and "custom_components" in module:
|
||||
return None
|
||||
|
||||
if integration:
|
||||
integration_domain = integration.domain
|
||||
|
||||
if integration_domain:
|
||||
issue_tracker += f"+label%3A%22integration%3A+{integration_domain}%22"
|
||||
return issue_tracker
|
||||
@@ -1416,15 +1465,21 @@ def async_get_issue_tracker(
|
||||
def async_suggest_report_issue(
|
||||
hass: HomeAssistant | None,
|
||||
*,
|
||||
integration: Integration | None = None,
|
||||
integration_domain: str | None = None,
|
||||
module: str | None = None,
|
||||
) -> str:
|
||||
"""Generate a blurb asking the user to file a bug report."""
|
||||
issue_tracker = async_get_issue_tracker(
|
||||
hass, integration_domain=integration_domain, module=module
|
||||
hass,
|
||||
integration=integration,
|
||||
integration_domain=integration_domain,
|
||||
module=module,
|
||||
)
|
||||
|
||||
if not issue_tracker:
|
||||
if integration:
|
||||
integration_domain = integration.domain
|
||||
if not integration_domain:
|
||||
return "report it to the custom integration author"
|
||||
return (
|
||||
|
||||
@@ -30,7 +30,7 @@ habluetooth==2.4.2
|
||||
hass-nabucasa==0.78.0
|
||||
hassil==1.6.1
|
||||
home-assistant-bluetooth==1.12.0
|
||||
home-assistant-frontend==20240228.1
|
||||
home-assistant-frontend==20240306.0
|
||||
home-assistant-intents==2024.2.28
|
||||
httpx==0.27.0
|
||||
ifaddr==0.2.0
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.3.0b3"
|
||||
version = "2024.3.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
+17
-17
@@ -188,10 +188,10 @@ aio-georss-gdacs==0.9
|
||||
aioairq==0.3.2
|
||||
|
||||
# homeassistant.components.airzone_cloud
|
||||
aioairzone-cloud==0.3.8
|
||||
aioairzone-cloud==0.4.5
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.7.5
|
||||
aioairzone==0.7.6
|
||||
|
||||
# homeassistant.components.ambient_station
|
||||
aioambient==2024.01.0
|
||||
@@ -206,7 +206,7 @@ aioaseko==0.0.2
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2024.2.7
|
||||
aioautomower==2024.2.10
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==1.3.5
|
||||
@@ -514,7 +514,7 @@ aurorapy==0.2.7
|
||||
# avion==0.10
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==49
|
||||
axis==50
|
||||
|
||||
# homeassistant.components.azure_event_hub
|
||||
azure-eventhub==5.11.1
|
||||
@@ -603,7 +603,7 @@ boschshcpy==0.2.75
|
||||
boto3==1.33.13
|
||||
|
||||
# homeassistant.components.bring
|
||||
bring-api==0.4.1
|
||||
bring-api==0.5.5
|
||||
|
||||
# homeassistant.components.broadlink
|
||||
broadlink==0.18.3
|
||||
@@ -621,7 +621,7 @@ brunt==1.2.0
|
||||
bt-proximity==0.2.1
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==3.5.0
|
||||
bthome-ble==3.6.0
|
||||
|
||||
# homeassistant.components.bt_home_hub_5
|
||||
bthomehub5-devicelist==0.1.1
|
||||
@@ -1071,10 +1071,10 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.43
|
||||
holidays==0.44
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240228.1
|
||||
home-assistant-frontend==20240306.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.2.28
|
||||
@@ -1773,7 +1773,7 @@ pydiscovergy==3.0.0
|
||||
pydoods==1.0.2
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
pydrawise==2024.2.0
|
||||
pydrawise==2024.3.0
|
||||
|
||||
# homeassistant.components.android_ip_webcam
|
||||
pydroid-ipcam==2.0.0
|
||||
@@ -2036,7 +2036,7 @@ pyotgw==2.1.3
|
||||
pyotp==2.8.0
|
||||
|
||||
# homeassistant.components.overkiz
|
||||
pyoverkiz==1.13.7
|
||||
pyoverkiz==1.13.8
|
||||
|
||||
# homeassistant.components.openweathermap
|
||||
pyowm==3.2.0
|
||||
@@ -2182,7 +2182,7 @@ pyswitchbee==1.8.0
|
||||
pytautulli==23.1.1
|
||||
|
||||
# homeassistant.components.tedee
|
||||
pytedee-async==0.2.13
|
||||
pytedee-async==0.2.15
|
||||
|
||||
# homeassistant.components.tfiac
|
||||
pytfiac==0.4
|
||||
@@ -2285,7 +2285,7 @@ python-rabbitair==0.0.8
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==0.39.2
|
||||
python-roborock==0.40.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.36
|
||||
@@ -2433,7 +2433,7 @@ renault-api==0.2.1
|
||||
renson-endura-delta==1.7.1
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.8.8
|
||||
reolink-aio==0.8.9
|
||||
|
||||
# homeassistant.components.idteck_prox
|
||||
rfk101py==0.0.1
|
||||
@@ -2758,7 +2758,7 @@ uasiren==0.0.1
|
||||
ultraheat-api==0.5.7
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
unifi-discovery==1.1.7
|
||||
unifi-discovery==1.1.8
|
||||
|
||||
# homeassistant.components.unifi_direct
|
||||
unifi_ap==0.0.1
|
||||
@@ -2836,7 +2836,7 @@ watchdog==2.3.1
|
||||
waterfurnace==1.1.0
|
||||
|
||||
# homeassistant.components.weatherflow_cloud
|
||||
weatherflow4py==0.1.11
|
||||
weatherflow4py==0.1.12
|
||||
|
||||
# homeassistant.components.webmin
|
||||
webmin-xmlrpc==0.0.1
|
||||
@@ -2872,7 +2872,7 @@ xbox-webapi==2.0.11
|
||||
xiaomi-ble==0.25.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==2.12.1
|
||||
xknx==2.12.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknxproject==3.7.0
|
||||
@@ -2950,7 +2950,7 @@ zigpy-zigate==0.12.0
|
||||
zigpy-znp==0.12.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.63.2
|
||||
zigpy==0.63.4
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.4
|
||||
|
||||
+17
-17
@@ -167,10 +167,10 @@ aio-georss-gdacs==0.9
|
||||
aioairq==0.3.2
|
||||
|
||||
# homeassistant.components.airzone_cloud
|
||||
aioairzone-cloud==0.3.8
|
||||
aioairzone-cloud==0.4.5
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.7.5
|
||||
aioairzone==0.7.6
|
||||
|
||||
# homeassistant.components.ambient_station
|
||||
aioambient==2024.01.0
|
||||
@@ -185,7 +185,7 @@ aioaseko==0.0.2
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2024.2.7
|
||||
aioautomower==2024.2.10
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==1.3.5
|
||||
@@ -454,7 +454,7 @@ auroranoaa==0.0.3
|
||||
aurorapy==0.2.7
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==49
|
||||
axis==50
|
||||
|
||||
# homeassistant.components.azure_event_hub
|
||||
azure-eventhub==5.11.1
|
||||
@@ -514,7 +514,7 @@ bond-async==0.2.1
|
||||
boschshcpy==0.2.75
|
||||
|
||||
# homeassistant.components.bring
|
||||
bring-api==0.4.1
|
||||
bring-api==0.5.5
|
||||
|
||||
# homeassistant.components.broadlink
|
||||
broadlink==0.18.3
|
||||
@@ -529,7 +529,7 @@ brottsplatskartan==1.0.5
|
||||
brunt==1.2.0
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==3.5.0
|
||||
bthome-ble==3.6.0
|
||||
|
||||
# homeassistant.components.buienradar
|
||||
buienradar==1.0.5
|
||||
@@ -870,10 +870,10 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.43
|
||||
holidays==0.44
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240228.1
|
||||
home-assistant-frontend==20240306.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.2.28
|
||||
@@ -1375,7 +1375,7 @@ pydexcom==0.2.3
|
||||
pydiscovergy==3.0.0
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
pydrawise==2024.2.0
|
||||
pydrawise==2024.3.0
|
||||
|
||||
# homeassistant.components.android_ip_webcam
|
||||
pydroid-ipcam==2.0.0
|
||||
@@ -1578,7 +1578,7 @@ pyotgw==2.1.3
|
||||
pyotp==2.8.0
|
||||
|
||||
# homeassistant.components.overkiz
|
||||
pyoverkiz==1.13.7
|
||||
pyoverkiz==1.13.8
|
||||
|
||||
# homeassistant.components.openweathermap
|
||||
pyowm==3.2.0
|
||||
@@ -1697,7 +1697,7 @@ pyswitchbee==1.8.0
|
||||
pytautulli==23.1.1
|
||||
|
||||
# homeassistant.components.tedee
|
||||
pytedee-async==0.2.13
|
||||
pytedee-async==0.2.15
|
||||
|
||||
# homeassistant.components.motionmount
|
||||
python-MotionMount==0.3.1
|
||||
@@ -1755,7 +1755,7 @@ python-qbittorrent==0.4.3
|
||||
python-rabbitair==0.0.8
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==0.39.2
|
||||
python-roborock==0.40.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.36
|
||||
@@ -1873,7 +1873,7 @@ renault-api==0.2.1
|
||||
renson-endura-delta==1.7.1
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.8.8
|
||||
reolink-aio==0.8.9
|
||||
|
||||
# homeassistant.components.rflink
|
||||
rflink==0.0.66
|
||||
@@ -2111,7 +2111,7 @@ uasiren==0.0.1
|
||||
ultraheat-api==0.5.7
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
unifi-discovery==1.1.7
|
||||
unifi-discovery==1.1.8
|
||||
|
||||
# homeassistant.components.zha
|
||||
universal-silabs-flasher==0.0.18
|
||||
@@ -2174,7 +2174,7 @@ wallbox==0.6.0
|
||||
watchdog==2.3.1
|
||||
|
||||
# homeassistant.components.weatherflow_cloud
|
||||
weatherflow4py==0.1.11
|
||||
weatherflow4py==0.1.12
|
||||
|
||||
# homeassistant.components.webmin
|
||||
webmin-xmlrpc==0.0.1
|
||||
@@ -2207,7 +2207,7 @@ xbox-webapi==2.0.11
|
||||
xiaomi-ble==0.25.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==2.12.1
|
||||
xknx==2.12.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknxproject==3.7.0
|
||||
@@ -2270,7 +2270,7 @@ zigpy-zigate==0.12.0
|
||||
zigpy-znp==0.12.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.63.2
|
||||
zigpy==0.63.4
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.55.3
|
||||
|
||||
@@ -46,6 +46,9 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None:
|
||||
) as mock_webserver, patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.login",
|
||||
return_value=None,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi._update_websockets",
|
||||
return_value=False,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -1380,10 +1380,147 @@ async def test_verify_group_supported_features(
|
||||
|
||||
assert len(hass.states.async_all()) == 4
|
||||
|
||||
assert hass.states.get("light.group").state == STATE_ON
|
||||
group_state = hass.states.get("light.group")
|
||||
assert group_state.state == STATE_ON
|
||||
assert group_state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP
|
||||
assert (
|
||||
hass.states.get("light.group").attributes[ATTR_SUPPORTED_FEATURES]
|
||||
group_state.attributes[ATTR_SUPPORTED_FEATURES]
|
||||
== LightEntityFeature.TRANSITION
|
||||
| LightEntityFeature.FLASH
|
||||
| LightEntityFeature.EFFECT
|
||||
)
|
||||
|
||||
|
||||
async def test_verify_group_color_mode_fallback(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket
|
||||
) -> None:
|
||||
"""Test that group supported features reflect what included lights support."""
|
||||
data = {
|
||||
"groups": {
|
||||
"43": {
|
||||
"action": {
|
||||
"alert": "none",
|
||||
"bri": 127,
|
||||
"colormode": "hs",
|
||||
"ct": 0,
|
||||
"effect": "none",
|
||||
"hue": 0,
|
||||
"on": True,
|
||||
"sat": 127,
|
||||
"scene": "4",
|
||||
"xy": [0, 0],
|
||||
},
|
||||
"devicemembership": [],
|
||||
"etag": "4548e982c4cfff942f7af80958abb2a0",
|
||||
"id": "43",
|
||||
"lights": ["13"],
|
||||
"name": "Opbergruimte",
|
||||
"scenes": [
|
||||
{
|
||||
"id": "1",
|
||||
"lightcount": 1,
|
||||
"name": "Scene Normaal deCONZ",
|
||||
"transitiontime": 10,
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"lightcount": 1,
|
||||
"name": "Scene Fel deCONZ",
|
||||
"transitiontime": 10,
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"lightcount": 1,
|
||||
"name": "Scene Gedimd deCONZ",
|
||||
"transitiontime": 10,
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"lightcount": 1,
|
||||
"name": "Scene Uit deCONZ",
|
||||
"transitiontime": 10,
|
||||
},
|
||||
],
|
||||
"state": {"all_on": False, "any_on": False},
|
||||
"type": "LightGroup",
|
||||
},
|
||||
},
|
||||
"lights": {
|
||||
"13": {
|
||||
"capabilities": {
|
||||
"alerts": [
|
||||
"none",
|
||||
"select",
|
||||
"lselect",
|
||||
"blink",
|
||||
"breathe",
|
||||
"okay",
|
||||
"channelchange",
|
||||
"finish",
|
||||
"stop",
|
||||
],
|
||||
"bri": {"min_dim_level": 5},
|
||||
},
|
||||
"config": {
|
||||
"bri": {"execute_if_off": True, "startup": "previous"},
|
||||
"groups": ["43"],
|
||||
"on": {"startup": "previous"},
|
||||
},
|
||||
"etag": "ca0ed7763eca37f5e6b24f6d46f8a518",
|
||||
"hascolor": False,
|
||||
"lastannounced": None,
|
||||
"lastseen": "2024-03-02T20:08Z",
|
||||
"manufacturername": "Signify Netherlands B.V.",
|
||||
"modelid": "LWA001",
|
||||
"name": "Opbergruimte Lamp Plafond",
|
||||
"productid": "Philips-LWA001-1-A19DLv5",
|
||||
"productname": "Hue white lamp",
|
||||
"state": {
|
||||
"alert": "none",
|
||||
"bri": 76,
|
||||
"effect": "none",
|
||||
"on": False,
|
||||
"reachable": True,
|
||||
},
|
||||
"swconfigid": "87169548",
|
||||
"swversion": "1.104.2",
|
||||
"type": "Dimmable light",
|
||||
"uniqueid": "00:17:88:01:08:11:22:33-01",
|
||||
},
|
||||
},
|
||||
}
|
||||
with patch.dict(DECONZ_WEB_REQUEST, data):
|
||||
await setup_deconz_integration(hass, aioclient_mock)
|
||||
|
||||
group_state = hass.states.get("light.opbergruimte")
|
||||
assert group_state.state == STATE_OFF
|
||||
assert group_state.attributes[ATTR_COLOR_MODE] is None
|
||||
|
||||
await mock_deconz_websocket(
|
||||
data={
|
||||
"e": "changed",
|
||||
"id": "13",
|
||||
"r": "lights",
|
||||
"state": {
|
||||
"alert": "none",
|
||||
"bri": 76,
|
||||
"effect": "none",
|
||||
"on": True,
|
||||
"reachable": True,
|
||||
},
|
||||
"t": "event",
|
||||
"uniqueid": "00:17:88:01:08:11:22:33-01",
|
||||
}
|
||||
)
|
||||
await mock_deconz_websocket(
|
||||
data={
|
||||
"e": "changed",
|
||||
"id": "43",
|
||||
"r": "groups",
|
||||
"state": {"all_on": True, "any_on": True},
|
||||
"t": "event",
|
||||
}
|
||||
)
|
||||
group_state = hass.states.get("light.opbergruimte")
|
||||
assert group_state.state == STATE_ON
|
||||
assert group_state.attributes[ATTR_COLOR_MODE] is ColorMode.UNKNOWN
|
||||
|
||||
@@ -4,6 +4,7 @@ import time
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aioautomower.utils import mower_list_to_dictionary_dataclass
|
||||
from aiohttp import ClientWebSocketResponse
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.application_credentials import (
|
||||
@@ -82,4 +83,11 @@ def mock_automower_client() -> Generator[AsyncMock, None, None]:
|
||||
client.get_status.return_value = mower_list_to_dictionary_dataclass(
|
||||
load_json_value_fixture("mower.json", DOMAIN)
|
||||
)
|
||||
|
||||
async def websocket_connect() -> ClientWebSocketResponse:
|
||||
"""Mock listen."""
|
||||
return ClientWebSocketResponse
|
||||
|
||||
client.auth = AsyncMock(side_effect=websocket_connect)
|
||||
|
||||
yield client
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Tests for init module."""
|
||||
from datetime import timedelta
|
||||
import http
|
||||
import time
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aioautomower.exceptions import ApiException, HusqvarnaWSServerHandshakeError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.husqvarna_automower.const import DOMAIN, OAUTH2_TOKEN
|
||||
@@ -11,7 +14,7 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
@@ -66,3 +69,42 @@ async def test_expired_token_refresh_failure(
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is expected_state
|
||||
|
||||
|
||||
async def test_update_failed(
|
||||
hass: HomeAssistant,
|
||||
mock_automower_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test load and unload entry."""
|
||||
getattr(mock_automower_client, "get_status").side_effect = ApiException(
|
||||
"Test error"
|
||||
)
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
|
||||
assert entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_websocket_not_available(
|
||||
hass: HomeAssistant,
|
||||
mock_automower_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test trying reload the websocket."""
|
||||
mock_automower_client.start_listening.side_effect = HusqvarnaWSServerHandshakeError(
|
||||
"Boom"
|
||||
)
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
assert "Failed to connect to websocket. Trying to reconnect: Boom" in caplog.text
|
||||
assert mock_automower_client.auth.websocket_connect.call_count == 1
|
||||
assert mock_automower_client.start_listening.call_count == 1
|
||||
assert mock_config_entry.state == ConfigEntryState.LOADED
|
||||
freezer.tick(timedelta(seconds=2))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_automower_client.auth.websocket_connect.call_count == 2
|
||||
assert mock_automower_client.start_listening.call_count == 2
|
||||
assert mock_config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
@@ -40,7 +40,9 @@ async def test_resolve(
|
||||
mock_api.get_item.side_effect = None
|
||||
mock_api.get_item.return_value = load_json_fixture("track.json")
|
||||
|
||||
play_media = await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/TRACK-UUID")
|
||||
play_media = await async_resolve_media(
|
||||
hass, f"{URI_SCHEME}{DOMAIN}/TRACK-UUID", "media_player.jellyfin_device"
|
||||
)
|
||||
|
||||
assert play_media.mime_type == "audio/flac"
|
||||
assert play_media.url == snapshot
|
||||
@@ -49,7 +51,9 @@ async def test_resolve(
|
||||
mock_api.get_item.side_effect = None
|
||||
mock_api.get_item.return_value = load_json_fixture("movie.json")
|
||||
|
||||
play_media = await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/MOVIE-UUID")
|
||||
play_media = await async_resolve_media(
|
||||
hass, f"{URI_SCHEME}{DOMAIN}/MOVIE-UUID", "media_player.jellyfin_device"
|
||||
)
|
||||
|
||||
assert play_media.mime_type == "video/mp4"
|
||||
assert play_media.url == snapshot
|
||||
@@ -59,7 +63,11 @@ async def test_resolve(
|
||||
mock_api.get_item.return_value = load_json_fixture("unsupported-item.json")
|
||||
|
||||
with pytest.raises(BrowseError):
|
||||
await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/UNSUPPORTED-ITEM-UUID")
|
||||
await async_resolve_media(
|
||||
hass,
|
||||
f"{URI_SCHEME}{DOMAIN}/UNSUPPORTED-ITEM-UUID",
|
||||
"media_player.jellyfin_device",
|
||||
)
|
||||
|
||||
|
||||
async def test_root(
|
||||
|
||||
@@ -121,17 +121,13 @@ async def test_async_resolve_media_no_entity(
|
||||
assert await async_setup_component(hass, media_source.DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
media = await media_source.async_resolve_media(
|
||||
hass,
|
||||
media_source.generate_media_source_id(media_source.DOMAIN, "local/test.mp3"),
|
||||
)
|
||||
assert isinstance(media, media_source.models.PlayMedia)
|
||||
assert media.url == "/media/local/test.mp3"
|
||||
assert media.mime_type == "audio/mpeg"
|
||||
assert (
|
||||
"calls media_source.async_resolve_media without passing an entity_id"
|
||||
in caplog.text
|
||||
)
|
||||
with pytest.raises(RuntimeError):
|
||||
await media_source.async_resolve_media(
|
||||
hass,
|
||||
media_source.generate_media_source_id(
|
||||
media_source.DOMAIN, "local/test.mp3"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def test_async_unresolve_media(hass: HomeAssistant) -> None:
|
||||
|
||||
@@ -153,7 +153,7 @@ async def test_setup_entry_lookup_failure(
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert java_mock_config_entry.state == ConfigEntryState.SETUP_ERROR
|
||||
assert java_mock_config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_setup_entry_init_failure(
|
||||
|
||||
@@ -740,6 +740,133 @@ async def test_duplicate_fan_mode_validator(do_config) -> None:
|
||||
assert len(do_config[CONF_FAN_MODE_VALUES]) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("do_config", "sensor_cnt"),
|
||||
[
|
||||
(
|
||||
[
|
||||
{
|
||||
CONF_NAME: TEST_MODBUS_NAME,
|
||||
CONF_TYPE: TCP,
|
||||
CONF_HOST: TEST_MODBUS_HOST,
|
||||
CONF_PORT: TEST_PORT_TCP,
|
||||
CONF_TIMEOUT: 3,
|
||||
CONF_SENSORS: [
|
||||
{
|
||||
CONF_NAME: TEST_ENTITY_NAME,
|
||||
CONF_ADDRESS: 117,
|
||||
CONF_SLAVE: 0,
|
||||
},
|
||||
{
|
||||
CONF_NAME: TEST_ENTITY_NAME + "1",
|
||||
CONF_ADDRESS: 119,
|
||||
CONF_SLAVE: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
2,
|
||||
),
|
||||
(
|
||||
[
|
||||
{
|
||||
CONF_NAME: TEST_MODBUS_NAME,
|
||||
CONF_TYPE: TCP,
|
||||
CONF_HOST: TEST_MODBUS_HOST,
|
||||
CONF_PORT: TEST_PORT_TCP,
|
||||
CONF_TIMEOUT: 3,
|
||||
CONF_SENSORS: [
|
||||
{
|
||||
CONF_NAME: TEST_ENTITY_NAME,
|
||||
CONF_ADDRESS: 117,
|
||||
CONF_SLAVE: 0,
|
||||
},
|
||||
{
|
||||
CONF_NAME: TEST_ENTITY_NAME + "1",
|
||||
CONF_ADDRESS: 117,
|
||||
CONF_SLAVE: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
2,
|
||||
),
|
||||
(
|
||||
[
|
||||
{
|
||||
CONF_NAME: TEST_MODBUS_NAME,
|
||||
CONF_TYPE: TCP,
|
||||
CONF_HOST: TEST_MODBUS_HOST,
|
||||
CONF_PORT: TEST_PORT_TCP,
|
||||
CONF_TIMEOUT: 3,
|
||||
CONF_SENSORS: [
|
||||
{
|
||||
CONF_NAME: TEST_ENTITY_NAME,
|
||||
CONF_ADDRESS: 117,
|
||||
CONF_SLAVE: 0,
|
||||
},
|
||||
{
|
||||
CONF_NAME: TEST_ENTITY_NAME + "1",
|
||||
CONF_ADDRESS: 117,
|
||||
CONF_SLAVE: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
1,
|
||||
),
|
||||
(
|
||||
[
|
||||
{
|
||||
CONF_NAME: TEST_MODBUS_NAME,
|
||||
CONF_TYPE: TCP,
|
||||
CONF_HOST: TEST_MODBUS_HOST,
|
||||
CONF_PORT: TEST_PORT_TCP,
|
||||
CONF_TIMEOUT: 3,
|
||||
CONF_SENSORS: [
|
||||
{
|
||||
CONF_NAME: TEST_ENTITY_NAME,
|
||||
CONF_ADDRESS: 117,
|
||||
CONF_SLAVE: 0,
|
||||
},
|
||||
{
|
||||
CONF_NAME: TEST_ENTITY_NAME + "1",
|
||||
CONF_ADDRESS: 119,
|
||||
CONF_SLAVE: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
CONF_NAME: TEST_MODBUS_NAME + "1",
|
||||
CONF_TYPE: TCP,
|
||||
CONF_HOST: TEST_MODBUS_HOST,
|
||||
CONF_PORT: TEST_PORT_TCP,
|
||||
CONF_TIMEOUT: 3,
|
||||
CONF_SENSORS: [
|
||||
{
|
||||
CONF_NAME: TEST_ENTITY_NAME,
|
||||
CONF_ADDRESS: 117,
|
||||
CONF_SLAVE: 0,
|
||||
},
|
||||
{
|
||||
CONF_NAME: TEST_ENTITY_NAME,
|
||||
CONF_ADDRESS: 119,
|
||||
CONF_SLAVE: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
2,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_duplicate_addresses(do_config, sensor_cnt) -> None:
|
||||
"""Test duplicate entity validator."""
|
||||
check_config(do_config)
|
||||
use_inx = len(do_config) - 1
|
||||
assert len(do_config[use_inx][CONF_SENSORS]) == sensor_cnt
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"do_config",
|
||||
[
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
from typing import Any
|
||||
@@ -15,7 +16,7 @@ from homeassistant.components.rainbird.const import (
|
||||
ATTR_DURATION,
|
||||
DEFAULT_TRIGGER_TIME_MINUTES,
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -155,6 +156,31 @@ def setup_platforms(
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def aioclient_mock(hass: HomeAssistant) -> Generator[AiohttpClientMocker, None, None]:
|
||||
"""Context manager to mock aiohttp client."""
|
||||
mocker = AiohttpClientMocker()
|
||||
|
||||
def create_session():
|
||||
session = mocker.create_session(hass.loop)
|
||||
|
||||
async def close_session(event):
|
||||
"""Close session."""
|
||||
await session.close()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, close_session)
|
||||
return session
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.rainbird.async_create_clientsession",
|
||||
side_effect=create_session,
|
||||
), patch(
|
||||
"homeassistant.components.rainbird.config_flow.async_create_clientsession",
|
||||
side_effect=create_session,
|
||||
):
|
||||
yield mocker
|
||||
|
||||
|
||||
def rainbird_json_response(result: dict[str, str]) -> bytes:
|
||||
"""Create a fake API response."""
|
||||
return encryption.encrypt(
|
||||
|
||||
@@ -81,7 +81,9 @@ async def test_resolve(
|
||||
f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}"
|
||||
)
|
||||
|
||||
play_media = await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/{file_id}")
|
||||
play_media = await async_resolve_media(
|
||||
hass, f"{URI_SCHEME}{DOMAIN}/{file_id}", None
|
||||
)
|
||||
|
||||
assert play_media.mime_type == TEST_MIME_TYPE
|
||||
|
||||
@@ -245,7 +247,7 @@ async def test_browsing_errors(
|
||||
with pytest.raises(Unresolvable):
|
||||
await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/UNKNOWN")
|
||||
with pytest.raises(Unresolvable):
|
||||
await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/UNKNOWN")
|
||||
await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/UNKNOWN", None)
|
||||
|
||||
|
||||
async def test_browsing_not_loaded(
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
'coordinators': dict({
|
||||
'**REDACTED-0**': dict({
|
||||
'api': dict({
|
||||
'misc_info': dict({
|
||||
}),
|
||||
}),
|
||||
'roborock_device_info': dict({
|
||||
'device': dict({
|
||||
@@ -309,6 +311,8 @@
|
||||
}),
|
||||
'**REDACTED-1**': dict({
|
||||
'api': dict({
|
||||
'misc_info': dict({
|
||||
}),
|
||||
}),
|
||||
'roborock_device_info': dict({
|
||||
'device': dict({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for the diagnostics data provided by the Roborock integration."""
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from syrupy.filters import props
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -20,4 +21,4 @@ async def test_diagnostics(
|
||||
result = await get_diagnostics_for_config_entry(hass, hass_client, setup_entry)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result == snapshot
|
||||
assert result == snapshot(exclude=props("Nonce"))
|
||||
|
||||
@@ -25,6 +25,7 @@ from homeassistant.components.zha.core.helpers import get_zha_gateway
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
@@ -39,6 +40,8 @@ from .common import (
|
||||
)
|
||||
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
ON = 1
|
||||
OFF = 0
|
||||
IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8"
|
||||
@@ -91,27 +94,6 @@ def zigpy_cover_device(zigpy_device_mock):
|
||||
return zigpy_device_mock(endpoints)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def coordinator(hass, zigpy_device_mock, zha_device_joined):
|
||||
"""Test ZHA light platform."""
|
||||
|
||||
zigpy_device = zigpy_device_mock(
|
||||
{
|
||||
1: {
|
||||
SIG_EP_INPUT: [],
|
||||
SIG_EP_OUTPUT: [],
|
||||
SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT,
|
||||
}
|
||||
},
|
||||
ieee="00:15:8d:00:02:32:4f:32",
|
||||
nwk=0x0000,
|
||||
node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
|
||||
)
|
||||
zha_device = await zha_device_joined(zigpy_device)
|
||||
zha_device.available = True
|
||||
return zha_device
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def device_switch_1(hass, zigpy_device_mock, zha_device_joined):
|
||||
"""Test ZHA switch platform."""
|
||||
@@ -300,19 +282,41 @@ async def zigpy_device_tuya(hass, zigpy_device_mock, zha_device_joined):
|
||||
new=0,
|
||||
)
|
||||
async def test_zha_group_switch_entity(
|
||||
hass: HomeAssistant, device_switch_1, device_switch_2, coordinator
|
||||
hass: HomeAssistant,
|
||||
device_switch_1,
|
||||
device_switch_2,
|
||||
entity_registry: er.EntityRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the switch entity for a ZHA group."""
|
||||
|
||||
# make sure we can still get groups when counter entities exist
|
||||
entity_id = "sensor.coordinator_manufacturer_coordinator_model_counter_1"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is None
|
||||
|
||||
# Enable the entity.
|
||||
entity_registry.async_update_entity(entity_id, disabled_by=None)
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == "1"
|
||||
|
||||
zha_gateway = get_zha_gateway(hass)
|
||||
assert zha_gateway is not None
|
||||
zha_gateway.coordinator_zha_device = coordinator
|
||||
coordinator._zha_gateway = zha_gateway
|
||||
device_switch_1._zha_gateway = zha_gateway
|
||||
device_switch_2._zha_gateway = zha_gateway
|
||||
member_ieee_addresses = [device_switch_1.ieee, device_switch_2.ieee]
|
||||
member_ieee_addresses = [
|
||||
device_switch_1.ieee,
|
||||
device_switch_2.ieee,
|
||||
zha_gateway.coordinator_zha_device.ieee,
|
||||
]
|
||||
members = [
|
||||
GroupMember(device_switch_1.ieee, 1),
|
||||
GroupMember(device_switch_2.ieee, 1),
|
||||
GroupMember(zha_gateway.coordinator_zha_device.ieee, 1),
|
||||
]
|
||||
|
||||
# test creating a group with 2 members
|
||||
@@ -320,7 +324,7 @@ async def test_zha_group_switch_entity(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert zha_group is not None
|
||||
assert len(zha_group.members) == 2
|
||||
assert len(zha_group.members) == 3
|
||||
for member in zha_group.members:
|
||||
assert member.device.ieee in member_ieee_addresses
|
||||
assert member.group == zha_group
|
||||
@@ -333,12 +337,6 @@ async def test_zha_group_switch_entity(
|
||||
dev1_cluster_on_off = device_switch_1.device.endpoints[1].on_off
|
||||
dev2_cluster_on_off = device_switch_2.device.endpoints[1].on_off
|
||||
|
||||
await async_enable_traffic(hass, [device_switch_1, device_switch_2], enabled=False)
|
||||
await async_wait_for_updates(hass)
|
||||
|
||||
# test that the switches were created and that they are off
|
||||
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
|
||||
|
||||
# allow traffic to flow through the gateway and device
|
||||
await async_enable_traffic(hass, [device_switch_1, device_switch_2])
|
||||
await async_wait_for_updates(hass)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from collections import OrderedDict
|
||||
from datetime import date, datetime, timedelta
|
||||
import enum
|
||||
import logging
|
||||
import os
|
||||
from socket import _GLOBAL_DEFAULT_TIMEOUT
|
||||
from unittest.mock import Mock, patch
|
||||
@@ -986,7 +987,11 @@ def test_deprecated_with_default(caplog: pytest.LogCaptureFixture, schema) -> No
|
||||
deprecated_schema = vol.All(cv.deprecated("mars", default=False), schema)
|
||||
|
||||
test_data = {"mars": True}
|
||||
output = deprecated_schema(test_data.copy())
|
||||
with patch(
|
||||
"homeassistant.helpers.config_validation.get_integration_logger",
|
||||
return_value=logging.getLogger(__name__),
|
||||
):
|
||||
output = deprecated_schema(test_data.copy())
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[0].name == __name__
|
||||
assert (
|
||||
@@ -1062,21 +1067,19 @@ def test_deprecated_with_replacement_key_and_default(
|
||||
|
||||
|
||||
def test_deprecated_cant_find_module() -> None:
|
||||
"""Test if the current module cannot be inspected."""
|
||||
with patch("inspect.getmodule", return_value=None):
|
||||
# This used to raise.
|
||||
cv.deprecated(
|
||||
"mars",
|
||||
replacement_key="jupiter",
|
||||
default=False,
|
||||
)
|
||||
"""Test if the current module cannot be found."""
|
||||
# This used to raise.
|
||||
cv.deprecated(
|
||||
"mars",
|
||||
replacement_key="jupiter",
|
||||
default=False,
|
||||
)
|
||||
|
||||
with patch("inspect.getmodule", return_value=None):
|
||||
# This used to raise.
|
||||
cv.removed(
|
||||
"mars",
|
||||
default=False,
|
||||
)
|
||||
# This used to raise.
|
||||
cv.removed(
|
||||
"mars",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
def test_deprecated_or_removed_logger_with_config_attributes(
|
||||
@@ -1551,8 +1554,7 @@ def test_empty_schema(caplog: pytest.LogCaptureFixture) -> None:
|
||||
|
||||
def test_empty_schema_cant_find_module() -> None:
|
||||
"""Test if the current module cannot be inspected."""
|
||||
with patch("inspect.getmodule", return_value=None):
|
||||
cv.empty_config_schema("test_domain")({"test_domain": {"foo": "bar"}})
|
||||
cv.empty_config_schema("test_domain")({"test_domain": {"foo": "bar"}})
|
||||
|
||||
|
||||
def test_config_entry_only_schema(
|
||||
@@ -1582,10 +1584,7 @@ def test_config_entry_only_schema(
|
||||
|
||||
def test_config_entry_only_schema_cant_find_module() -> None:
|
||||
"""Test if the current module cannot be inspected."""
|
||||
with patch("inspect.getmodule", return_value=None):
|
||||
cv.config_entry_only_config_schema("test_domain")(
|
||||
{"test_domain": {"foo": "bar"}}
|
||||
)
|
||||
cv.config_entry_only_config_schema("test_domain")({"test_domain": {"foo": "bar"}})
|
||||
|
||||
|
||||
def test_config_entry_only_schema_no_hass(
|
||||
|
||||
@@ -22,6 +22,14 @@ async def test_extract_frame_integration(
|
||||
)
|
||||
|
||||
|
||||
async def test_get_integration_logger(
|
||||
caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
|
||||
) -> None:
|
||||
"""Test extracting the current frame to get the logger."""
|
||||
logger = frame.get_integration_logger(__name__)
|
||||
assert logger.name == "homeassistant.components.hue"
|
||||
|
||||
|
||||
async def test_extract_frame_resolve_module(
|
||||
hass: HomeAssistant, enable_custom_integrations
|
||||
) -> None:
|
||||
@@ -39,6 +47,17 @@ async def test_extract_frame_resolve_module(
|
||||
)
|
||||
|
||||
|
||||
async def test_get_integration_logger_resolve_module(
|
||||
hass: HomeAssistant, enable_custom_integrations
|
||||
) -> None:
|
||||
"""Test getting the logger from integration context."""
|
||||
from custom_components.test_integration_frame import call_get_integration_logger
|
||||
|
||||
logger = call_get_integration_logger(__name__)
|
||||
|
||||
assert logger.name == "custom_components.test_integration_frame"
|
||||
|
||||
|
||||
async def test_extract_frame_integration_with_excluded_integration(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
@@ -102,6 +121,30 @@ async def test_extract_frame_no_integration(caplog: pytest.LogCaptureFixture) ->
|
||||
frame.get_integration_frame()
|
||||
|
||||
|
||||
async def test_get_integration_logger_no_integration(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test getting fallback logger without integration context."""
|
||||
with patch(
|
||||
"homeassistant.helpers.frame.extract_stack",
|
||||
return_value=[
|
||||
Mock(
|
||||
filename="/home/paulus/homeassistant/core.py",
|
||||
lineno="23",
|
||||
line="do_something()",
|
||||
),
|
||||
Mock(
|
||||
filename="/home/paulus/aiohue/lights.py",
|
||||
lineno="2",
|
||||
line="something()",
|
||||
),
|
||||
],
|
||||
):
|
||||
logger = frame.get_integration_logger(__name__)
|
||||
|
||||
assert logger.name == __name__
|
||||
|
||||
|
||||
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
|
||||
async def test_prevent_flooding(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
|
||||
|
||||
@@ -4,6 +4,7 @@ import sys
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
import pytest
|
||||
|
||||
from homeassistant import loader
|
||||
@@ -163,6 +164,57 @@ async def test_custom_integration_version_not_valid(
|
||||
) in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"blocked_versions",
|
||||
[
|
||||
loader.BlockedIntegration(None, "breaks Home Assistant"),
|
||||
loader.BlockedIntegration(AwesomeVersion("2.0.0"), "breaks Home Assistant"),
|
||||
],
|
||||
)
|
||||
async def test_custom_integration_version_blocked(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
enable_custom_integrations: None,
|
||||
blocked_versions,
|
||||
) -> None:
|
||||
"""Test that we log a warning when custom integrations have a blocked version."""
|
||||
with patch.dict(
|
||||
loader.BLOCKED_CUSTOM_INTEGRATIONS, {"test_blocked_version": blocked_versions}
|
||||
):
|
||||
with pytest.raises(loader.IntegrationNotFound):
|
||||
await loader.async_get_integration(hass, "test_blocked_version")
|
||||
|
||||
assert (
|
||||
"Version 1.0.0 of custom integration 'test_blocked_version' breaks"
|
||||
" Home Assistant and was blocked from loading, please report it to the"
|
||||
" author of the 'test_blocked_version' custom integration"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"blocked_versions",
|
||||
[
|
||||
loader.BlockedIntegration(AwesomeVersion("0.9.9"), "breaks Home Assistant"),
|
||||
loader.BlockedIntegration(AwesomeVersion("1.0.0"), "breaks Home Assistant"),
|
||||
],
|
||||
)
|
||||
async def test_custom_integration_version_not_blocked(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
enable_custom_integrations: None,
|
||||
blocked_versions,
|
||||
) -> None:
|
||||
"""Test that we log a warning when custom integrations have a blocked version."""
|
||||
with patch.dict(
|
||||
loader.BLOCKED_CUSTOM_INTEGRATIONS, {"test_blocked_version": blocked_versions}
|
||||
):
|
||||
await loader.async_get_integration(hass, "test_blocked_version")
|
||||
|
||||
assert (
|
||||
"Version 1.0.0 of custom integration 'test_blocked_version'"
|
||||
) not in caplog.text
|
||||
|
||||
|
||||
async def test_get_integration(hass: HomeAssistant) -> None:
|
||||
"""Test resolving integration."""
|
||||
with pytest.raises(loader.IntegrationNotLoaded):
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"domain": "test_blocked_version",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
"""An integration which calls helpers.frame.get_integration_frame."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers import frame
|
||||
|
||||
|
||||
def call_get_integration_logger(fallback_name: str) -> logging.Logger:
|
||||
"""Call get_integration_logger."""
|
||||
return frame.get_integration_logger(fallback_name)
|
||||
|
||||
|
||||
def call_get_integration_frame() -> frame.IntegrationFrame:
|
||||
"""Call get_integration_frame."""
|
||||
return frame.get_integration_frame()
|
||||
|
||||
Reference in New Issue
Block a user