Compare commits

...

53 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 bb6f8b9d57 Bump version to 2024.3.0b5 2024-03-02 22:09:17 -05:00
J. Nick Koston 780f6e8974 Avoid expensive inspect calls in config validators (#112085)
* Avoid expensive inspect calls in config validators

inspect has a performance problem https://github.com/python/cpython/issues/92041

We now avoid calling inspect unless we are going to log

* remove unused

* reduce

* get_integration_logger
2024-03-02 22:08:59 -05:00
J. Nick Koston ab30d44184 Fix executor being overloaded in caldav (#112084)
Migrate to using a single executor job instead of creating
one per calendar. If the user had a lot of calendars the
executor would get overloaded
2024-03-02 22:08:58 -05:00
J. Nick Koston e23f737fa7 Fix bootstrap being fetched three times during unifiprotect startup (#112082)
We always fetch it to check if the device is online.
Avoid fetching it again for migration by passing
it to the migrators
2024-03-02 22:08:57 -05:00
J. Nick Koston b8e3bb8eb8 Ensure all homekit_controller controllers are imported in advance (#112079)
* Ensure all homekit_controllers are imported in advance

We want to avoid importing them in the event loop later

* Ensure all homekit_controllers are imported in advance

We want to avoid importing them in the event loop later
2024-03-02 22:08:56 -05:00
elmurato 12574bca8b Fix setup failure due to temporary DNS issue in Minecraft Server (#112068)
Change ConfigEntryError to ConfigEntryNotReady on failed init
2024-03-02 22:08:55 -05:00
David Knowles f16ea54b4f Bump pydrawise to 2024.3.0 (#112066) 2024-03-02 22:08:54 -05:00
Paulus Schoutsen ad52bf608f Only load camera prefs once (#112064) 2024-03-02 22:08:53 -05:00
Isak Nyberg 46ee52f4ef Add device class for permobil record distance sensor (#112062)
fix record_distance device_class
2024-03-02 22:08:53 -05:00
Shay Levy 88fb44bbba Bump bthome-ble to 3.6.0 (#112060)
* Bump bthome-ble to 3.6.0

* Fix discovery info typing
2024-03-02 22:07:59 -05:00
J. Nick Koston de5e626430 Bump unifi-discovery to 1.1.8 (#112056) 2024-03-02 22:03:13 -05:00
J. Nick Koston 1bcdba1b4b Import anonymize_data in unifiprotect init to avoid it being imported in the event loop (#112052)
Improve anonymize_data in unifiprotect init to avoid it being imported in the event loop
2024-03-02 22:03:12 -05:00
Paulus Schoutsen a4353cf39d Bump version to 2024.3.0b4 2024-03-02 13:24:12 -05:00
Jeef 63192f2291 Bump weatherflow4py to v0.1.12 (#112040)
Backing lib bump
2024-03-02 13:24:05 -05:00
Joakim Sørensen 675b7ca7ba Fix config schema for velux (#112037) 2024-03-02 13:24:05 -05:00
Joakim Sørensen df5eb552a0 Use description key instead of name for Tibber RT unique ID (#112035)
* Use translation key instead of name for Tibber RT unique ID

* migration

* use decription.key instead
2024-03-02 13:24:04 -05:00
Álvaro Fernández Rojas 5017f4a2c7 Update aioairzone-cloud to v0.4.5 (#112034)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2024-03-02 13:24:03 -05:00
Chris Helming 92d3dccb94 Fix minor language issues in strings.json (#112006)
language fix: allow -> allows
2024-03-02 13:24:01 -05:00
David F. Mulcahey 2c38b5ee7b Bump Zigpy to 0.63.3 (#112002) 2024-03-02 13:24:00 -05:00
Paulus Schoutsen 435bb50d29 Update reporting for media_source.async_resolve_media (#111969)
* Update reporting for media_source.async_resolve_media

* Don't raise on core

* Fix tests
2024-03-02 13:23:59 -05:00
Paul Bottein 005493bb5a Update frontend to 20240301.0 (#111961) 2024-03-02 13:23:59 -05:00
Mick Vleeshouwer 838a4e4f7b Bump pyOverkiz to 1.13.8 (#111930)
Bump pyoverkiz to 1.13.8
2024-03-02 13:23:58 -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
75 changed files with 1004 additions and 309 deletions
@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.7.5"]
"requirements": ["aioairzone==0.7.6"]
}
@@ -24,6 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
options = ConnectionOptions(
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
True,
)
airzone = AirzoneCloudApi(aiohttp_client.async_get_clientsession(hass), options)
@@ -94,6 +94,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
ConnectionOptions(
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
False,
),
)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_polling",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.3.8"]
"requirements": ["aioairzone-cloud==0.4.5"]
}
+1 -1
View File
@@ -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]]):
+1 -1
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"]
}
+63 -40
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()
@@ -1,4 +1,5 @@
"""Config flow for BTHome Bluetooth integration."""
from __future__ import annotations
from collections.abc import Mapping
@@ -11,7 +12,7 @@ import voluptuous as vol
from homeassistant.components import onboarding
from homeassistant.components.bluetooth import (
BluetoothServiceInfo,
BluetoothServiceInfoBleak,
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigFlow
@@ -26,11 +27,11 @@ class Discovery:
"""A discovered bluetooth device."""
title: str
discovery_info: BluetoothServiceInfo
discovery_info: BluetoothServiceInfoBleak
device: DeviceData
def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str:
def _title(discovery_info: BluetoothServiceInfoBleak, device: DeviceData) -> str:
return device.title or device.get_device_name() or discovery_info.name
@@ -41,12 +42,12 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovery_info: BluetoothServiceInfo | None = None
self._discovery_info: BluetoothServiceInfoBleak | None = None
self._discovered_device: DeviceData | None = None
self._discovered_devices: dict[str, Discovery] = {}
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfo
self, discovery_info: BluetoothServiceInfoBleak
) -> FlowResult:
"""Handle the bluetooth discovery step."""
await self.async_set_unique_id(discovery_info.address)
@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==3.5.0"]
"requirements": ["bthome-ble==3.6.0"]
}
+6 -14
View File
@@ -1,6 +1,5 @@
"""Library for working with CalDAV api."""
import asyncio
import caldav
@@ -13,20 +12,13 @@ async def async_get_calendars(
"""Get all calendars that support the specified component."""
def _get_calendars() -> list[caldav.Calendar]:
return client.principal().calendars()
calendars = await hass.async_add_executor_job(_get_calendars)
components_results = await asyncio.gather(
*[
hass.async_add_executor_job(calendar.get_supported_components)
for calendar in calendars
return [
calendar
for calendar in client.principal().calendars()
if component in calendar.get_supported_components()
]
)
return [
calendar
for calendar, supported_components in zip(calendars, components_results)
if component in supported_components
]
return await hass.async_add_executor_job(_get_calendars)
def get_attr_value(obj: caldav.CalendarObjectResource, attribute: str) -> str | None:
@@ -391,6 +391,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
prefs = CameraPreferences(hass)
await prefs.async_load()
hass.data[DATA_CAMERA_PREFS] = prefs
hass.http.register_view(CameraImageView(component))
+10 -5
View File
@@ -29,6 +29,8 @@ class DynamicStreamSettings:
class CameraPreferences:
"""Handle camera preferences."""
_preload_prefs: dict[str, dict[str, bool | Orientation]]
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize camera prefs."""
self._hass = hass
@@ -41,6 +43,10 @@ class CameraPreferences:
str, DynamicStreamSettings
] = {}
async def async_load(self) -> None:
"""Initialize the camera preferences."""
self._preload_prefs = await self._store.async_load() or {}
async def async_update(
self,
entity_id: str,
@@ -63,9 +69,8 @@ class CameraPreferences:
if preload_stream is not UNDEFINED:
if dynamic_stream_settings:
dynamic_stream_settings.preload_stream = preload_stream
preload_prefs = await self._store.async_load() or {}
preload_prefs[entity_id] = {PREF_PRELOAD_STREAM: preload_stream}
await self._store.async_save(preload_prefs)
self._preload_prefs[entity_id] = {PREF_PRELOAD_STREAM: preload_stream}
await self._store.async_save(self._preload_prefs)
if orientation is not UNDEFINED:
if (registry := er.async_get(self._hass)).async_get(entity_id):
@@ -91,10 +96,10 @@ class CameraPreferences:
# Get orientation setting from entity registry
reg_entry = er.async_get(self._hass).async_get(entity_id)
er_prefs: Mapping = reg_entry.options.get(DOMAIN, {}) if reg_entry else {}
preload_prefs = await self._store.async_load() or {}
settings = DynamicStreamSettings(
preload_stream=cast(
bool, preload_prefs.get(entity_id, {}).get(PREF_PRELOAD_STREAM, False)
bool,
self._preload_prefs.get(entity_id, {}).get(PREF_PRELOAD_STREAM, False),
),
orientation=er_prefs.get(PREF_ORIENTATION, Orientation.NO_TRANSFORM),
)
+1
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."""
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20240228.1"]
"requirements": ["home-assistant-frontend==20240306.0"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.43", "babel==2.13.1"]
"requirements": ["holidays==0.44", "babel==2.13.1"]
}
@@ -6,6 +6,11 @@ import contextlib
import logging
import aiohomekit
from aiohomekit.const import (
BLE_TRANSPORT_SUPPORTED,
COAP_TRANSPORT_SUPPORTED,
IP_TRANSPORT_SUPPORTED,
)
from aiohomekit.exceptions import (
AccessoryDisconnectedError,
AccessoryNotFoundError,
@@ -24,6 +29,15 @@ from .connection import HKDevice
from .const import DOMAIN, KNOWN_DEVICES
from .utils import async_get_controller
# Ensure all the controllers get imported in the executor
# since they are loaded late.
if BLE_TRANSPORT_SUPPORTED:
from aiohomekit.controller import ble # noqa: F401
if COAP_TRANSPORT_SUPPORTED:
from aiohomekit.controller import coap # noqa: F401
if IP_TRANSPORT_SUPPORTED:
from aiohomekit.controller import ip # noqa: F401
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -6,7 +6,7 @@ from aioautomower.session import AutomowerSession
from aiohttp import ClientError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
@@ -17,7 +17,6 @@ from .coordinator import AutomowerDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.LAWN_MOWER, Platform.SENSOR, Platform.SWITCH]
@@ -38,13 +37,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await api_api.async_get_access_token()
except ClientError as err:
raise ConfigEntryNotReady from err
coordinator = AutomowerDataUpdateCoordinator(hass, automower_api)
coordinator = AutomowerDataUpdateCoordinator(hass, automower_api, entry)
await coordinator.async_config_entry_first_refresh()
entry.async_create_background_task(
hass,
coordinator.client_listen(hass, entry, automower_api),
"websocket_task",
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -52,8 +53,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle unload of an entry."""
coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
await coordinator.shutdown()
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
@@ -1,23 +1,28 @@
"""Data UpdateCoordinator for the Husqvarna Automower integration."""
import asyncio
from datetime import timedelta
import logging
from typing import Any
from aioautomower.exceptions import ApiException, HusqvarnaWSServerHandshakeError
from aioautomower.model import MowerAttributes
from aioautomower.session import AutomowerSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .api import AsyncConfigEntryAuth
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
MAX_WS_RECONNECT_TIME = 600
class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]):
"""Class to manage fetching Husqvarna data."""
def __init__(self, hass: HomeAssistant, api: AsyncConfigEntryAuth) -> None:
def __init__(
self, hass: HomeAssistant, api: AutomowerSession, entry: ConfigEntry
) -> None:
"""Initialize data updater."""
super().__init__(
hass,
@@ -35,13 +40,39 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
await self.api.connect()
self.api.register_data_callback(self.callback)
self.ws_connected = True
return await self.api.get_status()
async def shutdown(self, *_: Any) -> None:
"""Close resources."""
await self.api.close()
try:
return await self.api.get_status()
except ApiException as err:
raise UpdateFailed(err) from err
@callback
def callback(self, ws_data: dict[str, MowerAttributes]) -> None:
"""Process websocket callbacks and write them to the DataUpdateCoordinator."""
self.async_set_updated_data(ws_data)
async def client_listen(
self,
hass: HomeAssistant,
entry: ConfigEntry,
automower_client: AutomowerSession,
reconnect_time: int = 2,
) -> None:
"""Listen with the client."""
try:
await automower_client.auth.websocket_connect()
reconnect_time = 2
await automower_client.start_listening()
except HusqvarnaWSServerHandshakeError as err:
_LOGGER.debug(
"Failed to connect to websocket. Trying to reconnect: %s", err
)
if not hass.is_stopping:
await asyncio.sleep(reconnect_time)
reconnect_time = min(reconnect_time * 2, MAX_WS_RECONNECT_TIME)
await self.client_listen(
hass=hass,
entry=entry,
automower_client=automower_client,
reconnect_time=reconnect_time,
)
@@ -6,5 +6,5 @@
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
"iot_class": "cloud_push",
"requirements": ["aioautomower==2024.2.7"]
"requirements": ["aioautomower==2024.2.10"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2024.2.0"]
"requirements": ["pydrawise==2024.3.0"]
}
+1 -1
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"
]
@@ -148,7 +148,10 @@ async def async_resolve_media(
raise Unresolvable("Media Source not loaded")
if target_media_player is UNDEFINED:
report("calls media_source.async_resolve_media without passing an entity_id")
report(
"calls media_source.async_resolve_media without passing an entity_id",
{DOMAIN},
)
target_media_player = None
try:
@@ -14,7 +14,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryError
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.device_registry as dr
import homeassistant.helpers.entity_registry as er
@@ -41,9 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
await api.async_initialize()
except MinecraftServerAddressError as error:
raise ConfigEntryError(
f"Server address in configuration entry is invalid: {error}"
) from error
raise ConfigEntryNotReady(f"Initialization failed: {error}") from error
# Create coordinator instance.
coordinator = MinecraftServerCoordinator(hass, entry.data[CONF_NAME], api)
@@ -308,7 +308,7 @@ def check_config(config: dict) -> dict:
) -> bool:
"""Validate entity."""
name = entity[CONF_NAME]
addr = str(entity[CONF_ADDRESS])
addr = f"{hub_name}{entity[CONF_ADDRESS]}"
scan_interval = entity.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
if scan_interval < 5:
_LOGGER.warning(
@@ -335,11 +335,15 @@ def check_config(config: dict) -> dict:
loc_addr: set[str] = {addr}
if CONF_TARGET_TEMP in entity:
loc_addr.add(f"{entity[CONF_TARGET_TEMP]}_{inx}")
loc_addr.add(f"{hub_name}{entity[CONF_TARGET_TEMP]}_{inx}")
if CONF_HVAC_MODE_REGISTER in entity:
loc_addr.add(f"{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}")
loc_addr.add(
f"{hub_name}{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}"
)
if CONF_FAN_MODE_REGISTER in entity:
loc_addr.add(f"{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}")
loc_addr.add(
f"{hub_name}{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}"
)
dup_addrs = ent_addr.intersection(loc_addr)
if len(dup_addrs) > 0:
@@ -19,7 +19,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
"requirements": ["pyoverkiz==1.13.7"],
"requirements": ["pyoverkiz==1.13.8"],
"zeroconf": [
{
"type": "_kizbox._tcp.local.",
@@ -177,6 +177,7 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = (
key="record_distance",
translation_key="record_distance",
icon="mdi:map-marker-distance",
device_class=SensorDeviceClass.DISTANCE,
state_class=SensorStateClass.TOTAL_INCREASING,
),
)
@@ -11,11 +11,10 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from .const import CONF_SERIAL_NUMBER
from .coordinator import RainbirdData
from .coordinator import RainbirdData, async_create_clientsession
_LOGGER = logging.getLogger(__name__)
@@ -36,9 +35,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})
clientsession = async_create_clientsession()
entry.async_on_unload(clientsession.close)
controller = AsyncRainbirdController(
AsyncRainbirdClient(
async_get_clientsession(hass),
clientsession,
entry.data[CONF_HOST],
entry.data[CONF_PASSWORD],
)
@@ -20,7 +20,6 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv, selector
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from .const import (
@@ -30,6 +29,7 @@ from .const import (
DOMAIN,
TIMEOUT_SECONDS,
)
from .coordinator import async_create_clientsession
_LOGGER = logging.getLogger(__name__)
@@ -101,9 +101,10 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
Raises a ConfigFlowError on failure.
"""
clientsession = async_create_clientsession()
controller = AsyncRainbirdController(
AsyncRainbirdClient(
async_get_clientsession(self.hass),
clientsession,
host,
password,
)
@@ -124,6 +125,8 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
f"Error connecting to Rain Bird controller: {str(err)}",
"cannot_connect",
) from err
finally:
await clientsession.close()
async def async_finish(
self,
@@ -9,6 +9,7 @@ from functools import cached_property
import logging
from typing import TypeVar
import aiohttp
from pyrainbird.async_client import (
AsyncRainbirdController,
RainbirdApiException,
@@ -18,6 +19,7 @@ from pyrainbird.data import ModelAndVersion, Schedule
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -28,6 +30,13 @@ UPDATE_INTERVAL = datetime.timedelta(minutes=1)
# changes, so we refresh it less often.
CALENDAR_UPDATE_INTERVAL = datetime.timedelta(minutes=15)
# The valves state are not immediately reflected after issuing a command. We add
# small delay to give additional time to reflect the new state.
DEBOUNCER_COOLDOWN = 5
# Rainbird devices can only accept a single request at a time
CONECTION_LIMIT = 1
_LOGGER = logging.getLogger(__name__)
_T = TypeVar("_T")
@@ -43,6 +52,13 @@ class RainbirdDeviceState:
rain_delay: int
def async_create_clientsession() -> aiohttp.ClientSession:
"""Create a rainbird async_create_clientsession with a connection limit."""
return aiohttp.ClientSession(
connector=aiohttp.TCPConnector(limit=CONECTION_LIMIT),
)
class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
"""Coordinator for rainbird API calls."""
@@ -60,6 +76,9 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
_LOGGER,
name=name,
update_interval=UPDATE_INTERVAL,
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=DEBOUNCER_COOLDOWN, immediate=False
),
)
self._controller = controller
self._unique_id = unique_id
@@ -103,6 +103,10 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity)
except RainbirdApiException as err:
raise HomeAssistantError("Rain Bird device failure") from err
# The device reflects the old state for a few moments. Update the
# state manually and trigger a refresh after a short debounced delay.
self.coordinator.data.active_zones.add(self._zone)
self.async_write_ha_state()
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs):
@@ -115,6 +119,11 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity)
) from err
except RainbirdApiException as err:
raise HomeAssistantError("Rain Bird device failure") from err
# The device reflects the old state for a few moments. Update the
# state manually and trigger a refresh after a short debounced delay.
self.coordinator.data.active_zones.remove(self._zone)
self.async_write_ha_state()
await self.coordinator.async_request_refresh()
@property
+1 -1
View File
@@ -19,7 +19,7 @@
"title": "Random sensor"
},
"user": {
"description": "This helper allow you to create a helper that emits a random value.",
"description": "This helper allows you to create a helper that emits a random value.",
"menu_options": {
"binary_sensor": "Random binary sensor",
"sensor": "Random sensor"
@@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.8.8"]
"requirements": ["reolink-aio==0.8.9"]
}
@@ -367,7 +367,8 @@
"state": {
"stayoff": "Stay off",
"auto": "Auto",
"alwaysonatnight": "Auto & always on at night"
"alwaysonatnight": "Auto & always on at night",
"alwayson": "Always on"
}
}
},
@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": [
"python-roborock==0.39.2",
"python-roborock==0.40.0",
"vacuum-map-parser-roborock==0.1.1"
]
}
@@ -62,8 +62,11 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator):
station = await self._tankerkoenig.station_details(station_id)
except TankerkoenigInvalidKeyError as err:
raise ConfigEntryAuthFailed(err) from err
except (TankerkoenigError, TankerkoenigConnectionError) as err:
except TankerkoenigConnectionError as err:
raise ConfigEntryNotReady(err) from err
except TankerkoenigError as err:
_LOGGER.error("Error when adding station %s %s", station_id, err)
continue
self.stations[station_id] = station
+1 -1
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"]
}
@@ -20,7 +20,7 @@
"title": "Template sensor"
},
"user": {
"description": "This helper allow you to create helper entities that define their state using a template.",
"description": "This helper allows you to create helper entities that define their state using a template.",
"menu_options": {
"binary_sensor": "Template a binary sensor",
"sensor": "Template a sensor"
+76 -1
View File
@@ -60,6 +60,35 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
PARALLEL_UPDATES = 0
RT_SENSORS_UNIQUE_ID_MIGRATION = {
"accumulated_consumption_last_hour": "accumulated consumption current hour",
"accumulated_production_last_hour": "accumulated production current hour",
"current_l1": "current L1",
"current_l2": "current L2",
"current_l3": "current L3",
"estimated_hour_consumption": "Estimated consumption current hour",
}
RT_SENSORS_UNIQUE_ID_MIGRATION_SIMPLE = {
# simple migration can be done by replacing " " with "_"
"accumulated_consumption",
"accumulated_cost",
"accumulated_production",
"accumulated_reward",
"average_power",
"last_meter_consumption",
"last_meter_production",
"max_power",
"min_power",
"power_factor",
"power_production",
"signal_strength",
"voltage_phase1",
"voltage_phase2",
"voltage_phase3",
}
RT_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="averagePower",
@@ -454,7 +483,7 @@ class TibberSensorRT(TibberSensor, CoordinatorEntity["TibberRtDataCoordinator"])
self._device_name = f"{self._model} {self._home_name}"
self._attr_native_value = initial_state
self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.name}"
self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.key}"
if description.key in ("accumulatedCost", "accumulatedReward"):
self._attr_native_unit_of_measurement = tibber_home.currency
@@ -523,6 +552,7 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en
self._async_remove_device_updates_handler = self.async_add_listener(
self._add_sensors
)
self.entity_registry = async_get_entity_reg(hass)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
@callback
@@ -530,6 +560,49 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en
"""Handle Home Assistant stopping."""
self._async_remove_device_updates_handler()
@callback
def _migrate_unique_id(self, sensor_description: SensorEntityDescription) -> None:
"""Migrate unique id if needed."""
home_id = self._tibber_home.home_id
translation_key = sensor_description.translation_key
description_key = sensor_description.key
entity_id: str | None = None
if translation_key in RT_SENSORS_UNIQUE_ID_MIGRATION_SIMPLE:
entity_id = self.entity_registry.async_get_entity_id(
"sensor",
TIBBER_DOMAIN,
f"{home_id}_rt_{translation_key.replace('_', ' ')}",
)
elif translation_key in RT_SENSORS_UNIQUE_ID_MIGRATION:
entity_id = self.entity_registry.async_get_entity_id(
"sensor",
TIBBER_DOMAIN,
f"{home_id}_rt_{RT_SENSORS_UNIQUE_ID_MIGRATION[translation_key]}",
)
elif translation_key != description_key:
entity_id = self.entity_registry.async_get_entity_id(
"sensor",
TIBBER_DOMAIN,
f"{home_id}_rt_{translation_key}",
)
if entity_id is None:
return
new_unique_id = f"{home_id}_rt_{description_key}"
_LOGGER.debug(
"Migrating unique id for %s to %s",
entity_id,
new_unique_id,
)
try:
self.entity_registry.async_update_entity(
entity_id, new_unique_id=new_unique_id
)
except ValueError as err:
_LOGGER.error(err)
@callback
def _add_sensors(self) -> None:
"""Add sensor."""
@@ -543,6 +616,8 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en
state = live_measurement.get(sensor_description.key)
if state is None:
continue
self._migrate_unique_id(sensor_description)
entity = TibberSensorRT(
self._tibber_home,
sensor_description,
+4 -6
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()
@@ -5,8 +5,14 @@ from datetime import timedelta
import logging
from aiohttp.client_exceptions import ServerDisconnectedError
from pyunifiprotect.data import Bootstrap
from pyunifiprotect.exceptions import ClientError, NotAuthorized
# Import the test_util.anonymize module from the pyunifiprotect package
# in __init__ to ensure it gets imported in the executor since the
# diagnostics module will not be imported in the executor.
from pyunifiprotect.test_util.anonymize import anonymize_data # noqa: F401
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
@@ -123,7 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
try:
await _async_setup_entry(hass, entry, data_service)
await _async_setup_entry(hass, entry, data_service, bootstrap)
except Exception as err:
if await nvr_info.get_is_prerelease():
# If they are running a pre-release, its quite common for setup
@@ -151,9 +157,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, data_service: ProtectData
hass: HomeAssistant,
entry: ConfigEntry,
data_service: ProtectData,
bootstrap: Bootstrap,
) -> None:
await async_migrate_data(hass, entry, data_service.api)
await async_migrate_data(hass, entry, data_service.api, bootstrap)
await data_service.async_setup()
if not data_service.last_update_success:
@@ -42,7 +42,7 @@
"iot_class": "local_push",
"loggers": ["pyunifiprotect", "unifi_discovery"],
"quality_scale": "platinum",
"requirements": ["pyunifiprotect==4.23.3", "unifi-discovery==1.1.7"],
"requirements": ["pyunifiprotect==4.23.3", "unifi-discovery==1.1.8"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",
@@ -3,47 +3,39 @@ from __future__ import annotations
import logging
from aiohttp.client_exceptions import ServerDisconnectedError
from pyunifiprotect import ProtectApiClient
from pyunifiprotect.data import NVR, Bootstrap, ProtectAdoptableDeviceModel
from pyunifiprotect.exceptions import ClientError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
_LOGGER = logging.getLogger(__name__)
async def async_migrate_data(
hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient
hass: HomeAssistant,
entry: ConfigEntry,
protect: ProtectApiClient,
bootstrap: Bootstrap,
) -> None:
"""Run all valid UniFi Protect data migrations."""
_LOGGER.debug("Start Migrate: async_migrate_buttons")
await async_migrate_buttons(hass, entry, protect)
await async_migrate_buttons(hass, entry, protect, bootstrap)
_LOGGER.debug("Completed Migrate: async_migrate_buttons")
_LOGGER.debug("Start Migrate: async_migrate_device_ids")
await async_migrate_device_ids(hass, entry, protect)
await async_migrate_device_ids(hass, entry, protect, bootstrap)
_LOGGER.debug("Completed Migrate: async_migrate_device_ids")
async def async_get_bootstrap(protect: ProtectApiClient) -> Bootstrap:
"""Get UniFi Protect bootstrap or raise appropriate HA error."""
try:
bootstrap = await protect.get_bootstrap()
except (TimeoutError, ClientError, ServerDisconnectedError) as err:
raise ConfigEntryNotReady from err
return bootstrap
async def async_migrate_buttons(
hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient
hass: HomeAssistant,
entry: ConfigEntry,
protect: ProtectApiClient,
bootstrap: Bootstrap,
) -> None:
"""Migrate existing Reboot button unique IDs from {device_id} to {deivce_id}_reboot.
@@ -63,7 +55,6 @@ async def async_migrate_buttons(
_LOGGER.debug("No button entities need migration")
return
bootstrap = await async_get_bootstrap(protect)
count = 0
for button in to_migrate:
device = bootstrap.get_device_from_id(button.unique_id)
@@ -94,7 +85,10 @@ async def async_migrate_buttons(
async def async_migrate_device_ids(
hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient
hass: HomeAssistant,
entry: ConfigEntry,
protect: ProtectApiClient,
bootstrap: Bootstrap,
) -> None:
"""Migrate unique IDs from {device_id}_{name} format to {mac}_{name} format.
@@ -119,7 +113,6 @@ async def async_migrate_device_ids(
_LOGGER.debug("No entities need migration to MAC address ID")
return
bootstrap = await async_get_bootstrap(protect)
count = 0
for entity in to_migrate:
parts = entity.unique_id.split("_")
+2 -2
View File
@@ -22,8 +22,8 @@ CONFIG_SCHEMA = vol.Schema(
}
)
},
extra=vol.ALLOW_EXTRA,
)
),
extra=vol.ALLOW_EXTRA,
)
+2 -1
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
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud",
"iot_class": "cloud_polling",
"requirements": ["weatherflow4py==0.1.11"]
"requirements": ["weatherflow4py==0.1.12"]
}
@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.43"]
"requirements": ["holidays==0.44"]
}
@@ -87,6 +87,9 @@ class ZHAGroupMember(LogMixin):
entity_info = []
for entity_ref in zha_device_registry.get(self.device.ieee):
# We have device entities now that don't leverage cluster handlers
if not entity_ref.cluster_handlers:
continue
entity = entity_registry.async_get(entity_ref.reference_id)
handler = list(entity_ref.cluster_handlers.values())[0]
+1 -1
View File
@@ -27,7 +27,7 @@
"pyserial-asyncio==0.6",
"zha-quirks==0.0.112",
"zigpy-deconz==0.23.1",
"zigpy==0.63.2",
"zigpy==0.63.4",
"zigpy-xbee==0.20.1",
"zigpy-zigate==0.12.0",
"zigpy-znp==0.12.1",
+1 -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 = "0b3"
PATCH_VERSION: Final = "0"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
+11 -38
View File
@@ -10,7 +10,6 @@ from datetime import (
timedelta,
)
from enum import Enum, StrEnum
import inspect
import logging
from numbers import Number
import os
@@ -103,6 +102,7 @@ import homeassistant.util.dt as dt_util
from homeassistant.util.yaml.objects import NodeStrClass
from . import script_variables as script_variables_helper, template as template_helper
from .frame import get_integration_logger
TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM', 'HH:MM:SS' or 'HH:MM:SS.F'"
@@ -890,24 +890,17 @@ def _deprecated_or_removed(
- No warning if neither key nor replacement_key are provided
- Adds replacement_key with default value in this case
"""
module = inspect.getmodule(inspect.stack(context=0)[2].frame)
if module is not None:
module_name = module.__name__
else:
# If Python is unable to access the sources files, the call stack frame
# will be missing information, so let's guard.
# https://github.com/home-assistant/core/issues/24982
module_name = __name__
if option_removed:
logger_func = logging.getLogger(module_name).error
option_status = "has been removed"
else:
logger_func = logging.getLogger(module_name).warning
option_status = "is deprecated"
def validator(config: dict) -> dict:
"""Check if key is in config and log warning or error."""
if key in config:
if option_removed:
level = logging.ERROR
option_status = "has been removed"
else:
level = logging.WARNING
option_status = "is deprecated"
try:
near = (
f"near {config.__config_file__}" # type: ignore[attr-defined]
@@ -928,7 +921,7 @@ def _deprecated_or_removed(
if raise_if_present:
raise vol.Invalid(warning % arguments)
logger_func(warning, *arguments)
get_integration_logger(__name__).log(level, warning, *arguments)
value = config[key]
if replacement_key or option_removed:
config.pop(key)
@@ -1112,19 +1105,9 @@ def expand_condition_shorthand(value: Any | None) -> Any:
def empty_config_schema(domain: str) -> Callable[[dict], dict]:
"""Return a config schema which logs if there are configuration parameters."""
module = inspect.getmodule(inspect.stack(context=0)[2].frame)
if module is not None:
module_name = module.__name__
else:
# If Python is unable to access the sources files, the call stack frame
# will be missing information, so let's guard.
# https://github.com/home-assistant/core/issues/24982
module_name = __name__
logger_func = logging.getLogger(module_name).error
def validator(config: dict) -> dict:
if domain in config and config[domain]:
logger_func(
get_integration_logger(__name__).error(
(
"The %s integration does not support any configuration parameters, "
"got %s. Please remove the configuration parameters from your "
@@ -1146,16 +1129,6 @@ def _no_yaml_config_schema(
) -> Callable[[dict], dict]:
"""Return a config schema which logs if attempted to setup from YAML."""
module = inspect.getmodule(inspect.stack(context=0)[2].frame)
if module is not None:
module_name = module.__name__
else:
# If Python is unable to access the sources files, the call stack frame
# will be missing information, so let's guard.
# https://github.com/home-assistant/core/issues/24982
module_name = __name__
logger_func = logging.getLogger(module_name).error
def raise_issue() -> None:
# pylint: disable-next=import-outside-toplevel
from .issue_registry import IssueSeverity, async_create_issue
@@ -1176,7 +1149,7 @@ def _no_yaml_config_schema(
def validator(config: dict) -> dict:
if domain in config:
logger_func(
get_integration_logger(__name__).error(
(
"The %s integration does not support YAML setup, please remove it "
"from your configuration file"
+20
View File
@@ -34,6 +34,26 @@ class IntegrationFrame:
relative_filename: str
def get_integration_logger(fallback_name: str) -> logging.Logger:
"""Return a logger by checking the current integration frame.
If Python is unable to access the sources files, the call stack frame
will be missing information, so let's guard by requiring a fallback name.
https://github.com/home-assistant/core/issues/24982
"""
try:
integration_frame = get_integration_frame()
except MissingIntegrationFrame:
return logging.getLogger(fallback_name)
if integration_frame.custom_integration:
logger_name = f"custom_components.{integration_frame.integration}"
else:
logger_name = f"homeassistant.components.{integration_frame.integration}"
return logging.getLogger(logger_name)
def get_integration_frame(exclude_integrations: set | None = None) -> IntegrationFrame:
"""Return the frame, integration and integration path of the current stack frame."""
found_frame = None
+60 -5
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 (
+1 -1
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==20240228.1
home-assistant-frontend==20240306.0
home-assistant-intents==2024.2.28
httpx==0.27.0
ifaddr==0.2.0
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2024.3.0b3"
version = "2024.3.0"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
+17 -17
View File
@@ -188,10 +188,10 @@ aio-georss-gdacs==0.9
aioairq==0.3.2
# homeassistant.components.airzone_cloud
aioairzone-cloud==0.3.8
aioairzone-cloud==0.4.5
# homeassistant.components.airzone
aioairzone==0.7.5
aioairzone==0.7.6
# homeassistant.components.ambient_station
aioambient==2024.01.0
@@ -206,7 +206,7 @@ aioaseko==0.0.2
aioasuswrt==1.4.0
# homeassistant.components.husqvarna_automower
aioautomower==2024.2.7
aioautomower==2024.2.10
# homeassistant.components.azure_devops
aioazuredevops==1.3.5
@@ -514,7 +514,7 @@ aurorapy==0.2.7
# avion==0.10
# homeassistant.components.axis
axis==49
axis==50
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@@ -603,7 +603,7 @@ boschshcpy==0.2.75
boto3==1.33.13
# homeassistant.components.bring
bring-api==0.4.1
bring-api==0.5.5
# homeassistant.components.broadlink
broadlink==0.18.3
@@ -621,7 +621,7 @@ brunt==1.2.0
bt-proximity==0.2.1
# homeassistant.components.bthome
bthome-ble==3.5.0
bthome-ble==3.6.0
# homeassistant.components.bt_home_hub_5
bthomehub5-devicelist==0.1.1
@@ -1071,10 +1071,10 @@ hole==0.8.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.43
holidays==0.44
# homeassistant.components.frontend
home-assistant-frontend==20240228.1
home-assistant-frontend==20240306.0
# homeassistant.components.conversation
home-assistant-intents==2024.2.28
@@ -1773,7 +1773,7 @@ pydiscovergy==3.0.0
pydoods==1.0.2
# homeassistant.components.hydrawise
pydrawise==2024.2.0
pydrawise==2024.3.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==2.0.0
@@ -2036,7 +2036,7 @@ pyotgw==2.1.3
pyotp==2.8.0
# homeassistant.components.overkiz
pyoverkiz==1.13.7
pyoverkiz==1.13.8
# homeassistant.components.openweathermap
pyowm==3.2.0
@@ -2182,7 +2182,7 @@ pyswitchbee==1.8.0
pytautulli==23.1.1
# homeassistant.components.tedee
pytedee-async==0.2.13
pytedee-async==0.2.15
# homeassistant.components.tfiac
pytfiac==0.4
@@ -2285,7 +2285,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==0.39.2
python-roborock==0.40.0
# homeassistant.components.smarttub
python-smarttub==0.0.36
@@ -2433,7 +2433,7 @@ renault-api==0.2.1
renson-endura-delta==1.7.1
# homeassistant.components.reolink
reolink-aio==0.8.8
reolink-aio==0.8.9
# homeassistant.components.idteck_prox
rfk101py==0.0.1
@@ -2758,7 +2758,7 @@ uasiren==0.0.1
ultraheat-api==0.5.7
# homeassistant.components.unifiprotect
unifi-discovery==1.1.7
unifi-discovery==1.1.8
# homeassistant.components.unifi_direct
unifi_ap==0.0.1
@@ -2836,7 +2836,7 @@ watchdog==2.3.1
waterfurnace==1.1.0
# homeassistant.components.weatherflow_cloud
weatherflow4py==0.1.11
weatherflow4py==0.1.12
# homeassistant.components.webmin
webmin-xmlrpc==0.0.1
@@ -2872,7 +2872,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.25.2
# homeassistant.components.knx
xknx==2.12.1
xknx==2.12.2
# homeassistant.components.knx
xknxproject==3.7.0
@@ -2950,7 +2950,7 @@ zigpy-zigate==0.12.0
zigpy-znp==0.12.1
# homeassistant.components.zha
zigpy==0.63.2
zigpy==0.63.4
# homeassistant.components.zoneminder
zm-py==0.5.4
+17 -17
View File
@@ -167,10 +167,10 @@ aio-georss-gdacs==0.9
aioairq==0.3.2
# homeassistant.components.airzone_cloud
aioairzone-cloud==0.3.8
aioairzone-cloud==0.4.5
# homeassistant.components.airzone
aioairzone==0.7.5
aioairzone==0.7.6
# homeassistant.components.ambient_station
aioambient==2024.01.0
@@ -185,7 +185,7 @@ aioaseko==0.0.2
aioasuswrt==1.4.0
# homeassistant.components.husqvarna_automower
aioautomower==2024.2.7
aioautomower==2024.2.10
# homeassistant.components.azure_devops
aioazuredevops==1.3.5
@@ -454,7 +454,7 @@ auroranoaa==0.0.3
aurorapy==0.2.7
# homeassistant.components.axis
axis==49
axis==50
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@@ -514,7 +514,7 @@ bond-async==0.2.1
boschshcpy==0.2.75
# homeassistant.components.bring
bring-api==0.4.1
bring-api==0.5.5
# homeassistant.components.broadlink
broadlink==0.18.3
@@ -529,7 +529,7 @@ brottsplatskartan==1.0.5
brunt==1.2.0
# homeassistant.components.bthome
bthome-ble==3.5.0
bthome-ble==3.6.0
# homeassistant.components.buienradar
buienradar==1.0.5
@@ -870,10 +870,10 @@ hole==0.8.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.43
holidays==0.44
# homeassistant.components.frontend
home-assistant-frontend==20240228.1
home-assistant-frontend==20240306.0
# homeassistant.components.conversation
home-assistant-intents==2024.2.28
@@ -1375,7 +1375,7 @@ pydexcom==0.2.3
pydiscovergy==3.0.0
# homeassistant.components.hydrawise
pydrawise==2024.2.0
pydrawise==2024.3.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==2.0.0
@@ -1578,7 +1578,7 @@ pyotgw==2.1.3
pyotp==2.8.0
# homeassistant.components.overkiz
pyoverkiz==1.13.7
pyoverkiz==1.13.8
# homeassistant.components.openweathermap
pyowm==3.2.0
@@ -1697,7 +1697,7 @@ pyswitchbee==1.8.0
pytautulli==23.1.1
# homeassistant.components.tedee
pytedee-async==0.2.13
pytedee-async==0.2.15
# homeassistant.components.motionmount
python-MotionMount==0.3.1
@@ -1755,7 +1755,7 @@ python-qbittorrent==0.4.3
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==0.39.2
python-roborock==0.40.0
# homeassistant.components.smarttub
python-smarttub==0.0.36
@@ -1873,7 +1873,7 @@ renault-api==0.2.1
renson-endura-delta==1.7.1
# homeassistant.components.reolink
reolink-aio==0.8.8
reolink-aio==0.8.9
# homeassistant.components.rflink
rflink==0.0.66
@@ -2111,7 +2111,7 @@ uasiren==0.0.1
ultraheat-api==0.5.7
# homeassistant.components.unifiprotect
unifi-discovery==1.1.7
unifi-discovery==1.1.8
# homeassistant.components.zha
universal-silabs-flasher==0.0.18
@@ -2174,7 +2174,7 @@ wallbox==0.6.0
watchdog==2.3.1
# homeassistant.components.weatherflow_cloud
weatherflow4py==0.1.11
weatherflow4py==0.1.12
# homeassistant.components.webmin
webmin-xmlrpc==0.0.1
@@ -2207,7 +2207,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.25.2
# homeassistant.components.knx
xknx==2.12.1
xknx==2.12.2
# homeassistant.components.knx
xknxproject==3.7.0
@@ -2270,7 +2270,7 @@ zigpy-zigate==0.12.0
zigpy-znp==0.12.1
# homeassistant.components.zha
zigpy==0.63.2
zigpy==0.63.4
# homeassistant.components.zwave_js
zwave-js-server-python==0.55.3
@@ -46,6 +46,9 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None:
) as mock_webserver, patch(
"homeassistant.components.airzone_cloud.AirzoneCloudApi.login",
return_value=None,
), patch(
"homeassistant.components.airzone_cloud.AirzoneCloudApi._update_websockets",
return_value=False,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
+139 -2
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
@@ -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
+11 -3
View File
@@ -40,7 +40,9 @@ async def test_resolve(
mock_api.get_item.side_effect = None
mock_api.get_item.return_value = load_json_fixture("track.json")
play_media = await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/TRACK-UUID")
play_media = await async_resolve_media(
hass, f"{URI_SCHEME}{DOMAIN}/TRACK-UUID", "media_player.jellyfin_device"
)
assert play_media.mime_type == "audio/flac"
assert play_media.url == snapshot
@@ -49,7 +51,9 @@ async def test_resolve(
mock_api.get_item.side_effect = None
mock_api.get_item.return_value = load_json_fixture("movie.json")
play_media = await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/MOVIE-UUID")
play_media = await async_resolve_media(
hass, f"{URI_SCHEME}{DOMAIN}/MOVIE-UUID", "media_player.jellyfin_device"
)
assert play_media.mime_type == "video/mp4"
assert play_media.url == snapshot
@@ -59,7 +63,11 @@ async def test_resolve(
mock_api.get_item.return_value = load_json_fixture("unsupported-item.json")
with pytest.raises(BrowseError):
await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/UNSUPPORTED-ITEM-UUID")
await async_resolve_media(
hass,
f"{URI_SCHEME}{DOMAIN}/UNSUPPORTED-ITEM-UUID",
"media_player.jellyfin_device",
)
async def test_root(
+7 -11
View File
@@ -121,17 +121,13 @@ async def test_async_resolve_media_no_entity(
assert await async_setup_component(hass, media_source.DOMAIN, {})
await hass.async_block_till_done()
media = await media_source.async_resolve_media(
hass,
media_source.generate_media_source_id(media_source.DOMAIN, "local/test.mp3"),
)
assert isinstance(media, media_source.models.PlayMedia)
assert media.url == "/media/local/test.mp3"
assert media.mime_type == "audio/mpeg"
assert (
"calls media_source.async_resolve_media without passing an entity_id"
in caplog.text
)
with pytest.raises(RuntimeError):
await media_source.async_resolve_media(
hass,
media_source.generate_media_source_id(
media_source.DOMAIN, "local/test.mp3"
),
)
async def test_async_unresolve_media(hass: HomeAssistant) -> None:
@@ -153,7 +153,7 @@ async def test_setup_entry_lookup_failure(
)
await hass.async_block_till_done()
assert java_mock_config_entry.state == ConfigEntryState.SETUP_ERROR
assert java_mock_config_entry.state == ConfigEntryState.SETUP_RETRY
async def test_setup_entry_init_failure(
+127
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",
[
+27 -1
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(
@@ -81,7 +81,9 @@ async def test_resolve(
f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}"
)
play_media = await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/{file_id}")
play_media = await async_resolve_media(
hass, f"{URI_SCHEME}{DOMAIN}/{file_id}", None
)
assert play_media.mime_type == TEST_MIME_TYPE
@@ -245,7 +247,7 @@ async def test_browsing_errors(
with pytest.raises(Unresolvable):
await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/UNKNOWN")
with pytest.raises(Unresolvable):
await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/UNKNOWN")
await async_resolve_media(hass, f"{URI_SCHEME}{DOMAIN}/UNKNOWN", None)
async def test_browsing_not_loaded(
@@ -32,6 +32,8 @@
'coordinators': dict({
'**REDACTED-0**': dict({
'api': dict({
'misc_info': dict({
}),
}),
'roborock_device_info': dict({
'device': dict({
@@ -309,6 +311,8 @@
}),
'**REDACTED-1**': dict({
'api': dict({
'misc_info': dict({
}),
}),
'roborock_device_info': dict({
'device': dict({
@@ -1,6 +1,7 @@
"""Tests for the diagnostics data provided by the Roborock integration."""
from syrupy.assertion import SnapshotAssertion
from syrupy.filters import props
from homeassistant.core import HomeAssistant
@@ -20,4 +21,4 @@ async def test_diagnostics(
result = await get_diagnostics_for_config_entry(hass, hass_client, setup_entry)
assert isinstance(result, dict)
assert result == snapshot
assert result == snapshot(exclude=props("Nonce"))
+30 -32
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)
+20 -21
View File
@@ -2,6 +2,7 @@
from collections import OrderedDict
from datetime import date, datetime, timedelta
import enum
import logging
import os
from socket import _GLOBAL_DEFAULT_TIMEOUT
from unittest.mock import Mock, patch
@@ -986,7 +987,11 @@ def test_deprecated_with_default(caplog: pytest.LogCaptureFixture, schema) -> No
deprecated_schema = vol.All(cv.deprecated("mars", default=False), schema)
test_data = {"mars": True}
output = deprecated_schema(test_data.copy())
with patch(
"homeassistant.helpers.config_validation.get_integration_logger",
return_value=logging.getLogger(__name__),
):
output = deprecated_schema(test_data.copy())
assert len(caplog.records) == 1
assert caplog.records[0].name == __name__
assert (
@@ -1062,21 +1067,19 @@ def test_deprecated_with_replacement_key_and_default(
def test_deprecated_cant_find_module() -> None:
"""Test if the current module cannot be inspected."""
with patch("inspect.getmodule", return_value=None):
# This used to raise.
cv.deprecated(
"mars",
replacement_key="jupiter",
default=False,
)
"""Test if the current module cannot be found."""
# This used to raise.
cv.deprecated(
"mars",
replacement_key="jupiter",
default=False,
)
with patch("inspect.getmodule", return_value=None):
# This used to raise.
cv.removed(
"mars",
default=False,
)
# This used to raise.
cv.removed(
"mars",
default=False,
)
def test_deprecated_or_removed_logger_with_config_attributes(
@@ -1551,8 +1554,7 @@ def test_empty_schema(caplog: pytest.LogCaptureFixture) -> None:
def test_empty_schema_cant_find_module() -> None:
"""Test if the current module cannot be inspected."""
with patch("inspect.getmodule", return_value=None):
cv.empty_config_schema("test_domain")({"test_domain": {"foo": "bar"}})
cv.empty_config_schema("test_domain")({"test_domain": {"foo": "bar"}})
def test_config_entry_only_schema(
@@ -1582,10 +1584,7 @@ def test_config_entry_only_schema(
def test_config_entry_only_schema_cant_find_module() -> None:
"""Test if the current module cannot be inspected."""
with patch("inspect.getmodule", return_value=None):
cv.config_entry_only_config_schema("test_domain")(
{"test_domain": {"foo": "bar"}}
)
cv.config_entry_only_config_schema("test_domain")({"test_domain": {"foo": "bar"}})
def test_config_entry_only_schema_no_hass(
+43
View File
@@ -22,6 +22,14 @@ async def test_extract_frame_integration(
)
async def test_get_integration_logger(
caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
) -> None:
"""Test extracting the current frame to get the logger."""
logger = frame.get_integration_logger(__name__)
assert logger.name == "homeassistant.components.hue"
async def test_extract_frame_resolve_module(
hass: HomeAssistant, enable_custom_integrations
) -> None:
@@ -39,6 +47,17 @@ async def test_extract_frame_resolve_module(
)
async def test_get_integration_logger_resolve_module(
hass: HomeAssistant, enable_custom_integrations
) -> None:
"""Test getting the logger from integration context."""
from custom_components.test_integration_frame import call_get_integration_logger
logger = call_get_integration_logger(__name__)
assert logger.name == "custom_components.test_integration_frame"
async def test_extract_frame_integration_with_excluded_integration(
caplog: pytest.LogCaptureFixture,
) -> None:
@@ -102,6 +121,30 @@ async def test_extract_frame_no_integration(caplog: pytest.LogCaptureFixture) ->
frame.get_integration_frame()
async def test_get_integration_logger_no_integration(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test getting fallback logger without integration context."""
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
):
logger = frame.get_integration_logger(__name__)
assert logger.name == __name__
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
async def test_prevent_flooding(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
+52
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):
@@ -0,0 +1,4 @@
{
"domain": "test_blocked_version",
"version": "1.0.0"
}
@@ -1,8 +1,15 @@
"""An integration which calls helpers.frame.get_integration_frame."""
import logging
from homeassistant.helpers import frame
def call_get_integration_logger(fallback_name: str) -> logging.Logger:
"""Call get_integration_logger."""
return frame.get_integration_logger(fallback_name)
def call_get_integration_frame() -> frame.IntegrationFrame:
"""Call get_integration_frame."""
return frame.get_integration_frame()