forked from home-assistant/core
Compare commits
31 Commits
2024.3.0b5
...
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 | ||
|
|
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"]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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==20240301.0"]
|
||||
"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,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"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.3",
|
||||
"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 = "0b5"
|
||||
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)
|
||||
|
||||
@@ -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==20240301.0
|
||||
home-assistant-frontend==20240306.0
|
||||
home-assistant-intents==2024.2.28
|
||||
httpx==0.27.0
|
||||
ifaddr==0.2.0
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.3.0b5"
|
||||
version = "2024.3.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -191,7 +191,7 @@ aioairq==0.3.2
|
||||
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
|
||||
@@ -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==20240301.0
|
||||
home-assistant-frontend==20240306.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.2.28
|
||||
@@ -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
|
||||
@@ -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.3
|
||||
zigpy==0.63.4
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.4
|
||||
|
||||
@@ -170,7 +170,7 @@ aioairq==0.3.2
|
||||
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
|
||||
@@ -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==20240301.0
|
||||
home-assistant-frontend==20240306.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.2.28
|
||||
@@ -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
|
||||
@@ -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.3
|
||||
zigpy==0.63.4
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.55.3
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user