Compare commits

...

31 Commits

Author SHA1 Message Date
Franck Nijhof
1aa5a07501 2024.3.0 (#112516) 2024-03-06 18:52:11 +01:00
Franck Nijhof
efe9938b33 Bump version to 2024.3.0 2024-03-06 18:37:11 +01:00
Franck Nijhof
1b64989909 Bump version to 2024.3.0b8 2024-03-06 15:03:47 +01:00
Erik Montnemery
b480b68e3e Allow start_time >= 1.1.7 (#112500) 2024-03-06 15:03:23 +01:00
Josef Zweck
5294b492fc Bump pytedee_async to 0.2.15 (#112495) 2024-03-06 15:03:19 +01:00
Bram Kragten
080fe4cf5f Update frontend to 20240306.0 (#112492) 2024-03-06 15:03:16 +01:00
Erik Montnemery
8b2f40390b Add custom integration block list (#112481)
* Add custom integration block list

* Fix typo

* Add version condition

* Add block reason, simplify blocked versions, add tests

* Change logic for OK versions

* Add link to custom integration's issue tracker

* Add missing file

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-03-06 15:01:25 +01:00
Thomas55555
3b63719fad Avoid errors when there is no internet connection in Husqvarna Automower (#111101)
* Avoid errors when no internet connection

* Add error

* Create task in HA

* change from matter to automower

* tests

* Update homeassistant/components/husqvarna_automower/coordinator.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* address review

* Make websocket optional

* fix aioautomower version

* Fix tests

* Use stored websocket

* reset reconnect time after sucessful connection

* Typo

* Remove comment

* Add test

* Address review

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-03-06 14:58:08 +01:00
Paulus Schoutsen
061ae756ac Bump version to 2024.3.0b7 2024-03-05 23:43:11 -05:00
Matthias Alphart
862bd8ff07 Update xknx to 2.12.2 - Fix thread leak on unsuccessful connections (#112450)
Update xknx to 2.12.2
2024-03-05 23:43:07 -05:00
G Johansson
742710443a Bump holidays to 0.44 (#112442) 2024-03-05 23:43:06 -05:00
Robert Svensson
015aeadf88 Fix handling missing parameter by bumping axis library to v50 (#112437)
Fix handling missing parameter
2024-03-05 23:43:05 -05:00
Robert Svensson
b8b654a160 Do not use list comprehension in async_add_entities in Unifi (#112435)
Do not use list comprehension in async_add_entities
2024-03-05 23:43:04 -05:00
jan iversen
3c5b5ca49b Allow duplicate modbus addresses on different devices (#112434) 2024-03-05 23:43:04 -05:00
Mr. Bubbles
fb789d95ed Bump bring-api to 0.5.5 (#112266)
Fix KeyError listArticleLanguage
2024-03-05 23:43:03 -05:00
Álvaro Fernández Rojas
2e6906c8d4 Update aioairzone to v0.7.6 (#112264)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2024-03-05 23:43:02 -05:00
Luke Lashley
cc8d44bbd1 Bump python_roborock to 0.40.0 (#112238)
* bump to python_roborock 0.40.0

* manifest went away in merge?
2024-03-05 23:43:01 -05:00
Robert Svensson
0ad56de6fc Fix deCONZ light entity might not report a supported color mode (#112116)
* Handle case where deCONZ light entity might not report a supported color mode

* If in an unknown color mode set ColorMode.UNKNOWN

* Fix comment from external discussion
2024-03-05 23:43:00 -05:00
Paulus Schoutsen
dedd7a5a41 Bump version to 2024.3.0b6 2024-03-04 13:04:03 -05:00
Paul Bottein
44c961720c Update frontend to 20240304.0 (#112263) 2024-03-04 13:03:50 -05:00
Allen Porter
79b1d6df1b Add rainbird request debouncer and immediately update entity switch state (#112152) 2024-03-04 13:03:49 -05:00
Allen Porter
274ab2328e Limit rainbird aiohttp client session to a single connection (#112146)
Limit rainbird to a single open http connection
2024-03-04 13:03:48 -05:00
David F. Mulcahey
93ee900cb3 Fix ZHA groups page (#112140)
* Fix ZHA groups page

* test
2024-03-04 13:02:23 -05:00
Michael
62474967c9 Ignore failing gas stations in Tankerkoening (#112125) 2024-03-04 13:02:22 -05:00
starkillerOG
2cdc8d5f69 Bump reolink-aio to 0.8.9 (#112124)
* Update strings.json

* Bump reolink-aio to 0.8.9
2024-03-04 13:02:21 -05:00
David F. Mulcahey
4863c94824 Bump Zigpy to 0.63.4 (#112117) 2024-03-04 13:02:20 -05:00
Mr. Bubbles
193332da74 Bump bring-api to 0.5.4 (#111654) 2024-03-04 13:02:20 -05:00
Christopher Fenner
9926296d35 Handle exception in ViCare integration (#111128) 2024-03-04 13:02:19 -05:00
Paulus Schoutsen
bc47c80bbf 2024.2.5 (#111648) 2024-02-27 13:23:44 -05:00
Paulus Schoutsen
aabaa30fa7 2024.2.4 (#111441) 2024-02-26 11:17:13 -05:00
Franck Nijhof
1ee39275fc 2024.2.3 (#111133) 2024-02-22 16:08:18 +01:00
43 changed files with 707 additions and 157 deletions

View File

@@ -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"]
}

View File

@@ -26,7 +26,7 @@
"iot_class": "local_push",
"loggers": ["axis"],
"quality_scale": "platinum",
"requirements": ["axis==49"],
"requirements": ["axis==50"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

@@ -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]]):

View File

@@ -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"]
}

View File

@@ -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()

View File

@@ -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."""

View File

@@ -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"]
}

View File

@@ -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"]
}

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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"]
}

View File

@@ -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"
]

View File

@@ -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:

View File

@@ -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],
)

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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"]
}

View File

@@ -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"
}
}
},

View File

@@ -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"
]
}

View File

@@ -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

View File

@@ -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"]
}

View File

@@ -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()

View File

@@ -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

View File

@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.43"]
"requirements": ["holidays==0.44"]
}

View File

@@ -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]

View File

@@ -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",

View File

@@ -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)

View File

@@ -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 (

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",
[

View File

@@ -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(

View File

@@ -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({

View File

@@ -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"))

View File

@@ -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)

View File

@@ -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):

View File

@@ -0,0 +1,4 @@
{
"domain": "test_blocked_version",
"version": "1.0.0"
}