Compare commits

...

21 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
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
29 changed files with 530 additions and 67 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,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bring",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["bring-api==0.5.4"]
"requirements": ["bring-api==0.5.5"]
}

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==20240304.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

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

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

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

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 = "0b6"
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==20240304.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.0b6"
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.5.4
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==20240304.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
@@ -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

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.5.4
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==20240304.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
@@ -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

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

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

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