Compare commits

...

11 Commits

Author SHA1 Message Date
Michael fdb581ea7f Add missing exception translation keys in Ecovacs (#172658) 2026-05-31 17:35:03 +02:00
Mick Vleeshouwer 30f03dc01e Fix sentence-casing of Overkiz energy demand status binary sensor (#172653) 2026-05-31 13:48:09 +02:00
renovate[bot] 4f92c1686b Update pytest-socket to 0.8.0 (#172516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-31 13:40:59 +02:00
Jan Bouwhuis a676072e0d Fix MQTT device_tracker not saving state on location accuracy changes (#172629) 2026-05-31 12:03:12 +02:00
epenet 0ebcbf33ba Bump tuya-device-handlers to 0.0.22 (#172648) 2026-05-31 11:57:11 +02:00
Kamil Breguła cb544f2f67 Refresh WLED firmware releases on manual entity update (#172517)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-31 11:55:30 +02:00
TheJulianJES 26c5c37f53 Bump ZHA to 1.4.1 (#172640) 2026-05-31 11:22:47 +02:00
epenet b9ed8e91df Cleanup incorrect import path in Tuya coordinator (#172330) 2026-05-31 11:10:22 +02:00
alexborro 33a721245c Catch network errors during Loqed config entry unload (#172617) 2026-05-31 11:04:40 +02:00
Sören 840243db9c Improve Avea Bluetooth discovery flow (#172623) 2026-05-30 09:49:36 -05:00
Avi Miller 740778f00b fix: bump aiolifx and aiolifx-themes (#172619)
Signed-off-by: Avi Miller <me@dje.li>
2026-05-30 16:56:20 +03:00
17 changed files with 185 additions and 40 deletions
+13 -6
View File
@@ -8,6 +8,7 @@ import avea
from bleak.exc import BleakError
import voluptuous as vol
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
@@ -66,6 +67,15 @@ def _is_avea_discovery(discovery_info: BluetoothServiceInfoBleak) -> bool:
return AVEA_SERVICE_UUID in discovery_info.service_uuids
def _discovery_label(discovery_info: BluetoothServiceInfoBleak) -> str:
"""Return a label for a discovered Avea bulb."""
if (
name := _normalize_name(discovery_info.name)
) and name != discovery_info.address:
return f"{name} ({discovery_info.address})"
return discovery_info.address
class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Avea."""
@@ -150,6 +160,7 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
if discovery := self._discovery_info:
self._discovered_devices[discovery.address] = discovery
else:
await bluetooth.async_request_active_scan(self.hass)
current_addresses = self._async_current_ids(include_ignore=False)
for discovery in async_discovered_service_info(self.hass):
if (
@@ -165,11 +176,10 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
if self._discovery_info:
disc = self._discovery_info
label = f"{disc.name or disc.address} ({disc.address})"
data_schema = vol.Schema(
{
vol.Required(CONF_ADDRESS, default=disc.address): vol.In(
{disc.address: label}
{disc.address: _discovery_label(disc)}
)
}
)
@@ -178,10 +188,7 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
{
vol.Required(CONF_ADDRESS): vol.In(
{
service_info.address: (
f"{service_info.name or service_info.address}"
f" ({service_info.address})"
)
service_info.address: _discovery_label(service_info)
for service_info in self._discovered_devices.values()
}
),
@@ -294,6 +294,9 @@
"vacuum_raw_get_positions_not_supported": {
"message": "Retrieving the positions of the chargers and the device itself is not supported"
},
"vacuum_send_command_not_supported": {
"message": "The {command} command is not supported by {name}"
},
"vacuum_send_command_params_dict": {
"message": "Params must be a dictionary and not a list"
},
+2 -3
View File
@@ -353,11 +353,10 @@ class EcovacsVacuum(
if self._capability.clean.action.area is None:
info = self._device.device_info
name = info.get("nick", info["name"])
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="vacuum_send_command_area_not_supported",
translation_placeholders={"name": name},
translation_key="vacuum_send_command_not_supported",
translation_placeholders={"command": command, "name": name},
)
if command == "spot_area":
+2 -2
View File
@@ -53,8 +53,8 @@
"iot_class": "local_polling",
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
"requirements": [
"aiolifx==1.2.1",
"aiolifx==1.2.2",
"aiolifx-effects==0.3.2",
"aiolifx-themes==1.0.2"
"aiolifx-themes==1.0.4"
]
}
+13 -6
View File
@@ -4,6 +4,7 @@ import asyncio
import logging
from typing import TypedDict
import aiohttp
from aiohttp.web import Request
from loqedAPI import loqed
@@ -160,14 +161,20 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]):
_LOGGER.debug("Webhook URL: %s", webhook_url)
webhooks = await self.lock.getWebhooks()
try:
webhooks = await self.lock.getWebhooks()
webhook_index = next(
(x["id"] for x in webhooks if x["url"] == webhook_url), None
)
webhook_index = next(
(x["id"] for x in webhooks if x["url"] == webhook_url), None
)
if webhook_index:
await self.lock.deleteWebhook(webhook_index)
if webhook_index:
await self.lock.deleteWebhook(webhook_index)
except (TimeoutError, aiohttp.ClientError) as err:
_LOGGER.warning(
"Could not remove webhook from LOQED bridge; the bridge may be offline. Continuing to unload the entry anyway: %s",
err,
)
async def async_cloudhook_generate_url(
+1 -1
View File
@@ -530,7 +530,7 @@ class MqttAttributesMixin(Entity):
self._attributes_message_received,
{
"_attr_extra_state_attributes",
"_attr_gps_accuracy",
"_attr_location_accuracy",
"_attr_latitude",
"_attr_location_name",
"_attr_longitude",
@@ -96,7 +96,7 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [
# DomesticHotWaterProduction/WaterHeatingSystem
OverkizBinarySensorDescription(
key=OverkizState.IO_OPERATING_MODE_CAPABILITIES,
name="Energy Demand Status",
name="Energy demand status",
device_class=BinarySensorDeviceClass.HEAT,
value_fn=lambda state: (
cast(dict, state).get(OverkizCommandParam.ENERGY_DEMAND_STATUS) == 1
+2 -1
View File
@@ -3,7 +3,8 @@
from pathlib import Path
from typing import Any
from tuya_device_handlers.devices import TUYA_QUIRKS_REGISTRY, register_tuya_quirks
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from tuya_device_handlers.devices import register_tuya_quirks
from tuya_sharing import (
CustomerDevice,
Manager,
+1 -1
View File
@@ -44,7 +44,7 @@
"iot_class": "cloud_push",
"loggers": ["tuya_sharing"],
"requirements": [
"tuya-device-handlers==0.0.21",
"tuya-device-handlers==0.0.22",
"tuya-device-sharing-sdk==0.2.8"
]
}
+5
View File
@@ -112,3 +112,8 @@ class WLEDUpdateEntity(WLEDEntity, UpdateEntity):
version = cast(str, self.latest_version)
await self.coordinator.wled.upgrade(version=version)
await self.coordinator.async_refresh()
async def async_update(self) -> None:
"""Update the entity."""
await super().async_update()
await self.releases_coordinator.async_request_refresh()
+1 -1
View File
@@ -23,7 +23,7 @@
"universal_silabs_flasher",
"serialx"
],
"requirements": ["zha==1.4.0"],
"requirements": ["zha==1.4.1"],
"usb": [
{
"description": "*2652*",
+4 -4
View File
@@ -318,10 +318,10 @@ aiolichess==1.3.0
aiolifx-effects==0.3.2
# homeassistant.components.lifx
aiolifx-themes==1.0.2
aiolifx-themes==1.0.4
# homeassistant.components.lifx
aiolifx==1.2.1
aiolifx==1.2.2
# homeassistant.components.lookin
aiolookin==1.0.0
@@ -3207,7 +3207,7 @@ ttls==1.8.3
ttn_client==1.3.0
# homeassistant.components.tuya
tuya-device-handlers==0.0.21
tuya-device-handlers==0.0.22
# homeassistant.components.tuya
tuya-device-sharing-sdk==0.2.8
@@ -3442,7 +3442,7 @@ zeroconf==0.149.16
zeversolar==0.3.2
# homeassistant.components.zha
zha==1.4.0
zha==1.4.1
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13
+1 -1
View File
@@ -27,7 +27,7 @@ pytest-aiohttp==1.1.0
pytest-cov==7.1.0
pytest-freezer==0.4.9
pytest-github-actions-annotate-failures==0.4.0
pytest-socket==0.7.0
pytest-socket==0.8.0
pytest-sugar==1.1.1
pytest-timeout==2.4.0
pytest-unordered==0.7.0
+69 -8
View File
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch
import pytest
from homeassistant.components.avea.const import DOMAIN
from homeassistant.components.avea.const import AVEA_SERVICE_UUID, DOMAIN
from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_ADDRESS, CONF_NAME
from homeassistant.core import HomeAssistant
@@ -12,7 +12,11 @@ from homeassistant.data_entry_flow import FlowResultType
from . import AVEA_DISCOVERY_INFO, NOT_AVEA_DISCOVERY_INFO
from tests.components.bluetooth import inject_bluetooth_service_info
from tests.components.bluetooth import (
generate_advertisement_data,
generate_ble_device,
inject_bluetooth_service_info,
)
pytestmark = pytest.mark.usefixtures("enable_bluetooth")
@@ -35,13 +39,22 @@ async def test_user_step_success(hass: HomeAssistant) -> None:
inject_bluetooth_service_info(hass, AVEA_DISCOVERY_INFO)
await hass.async_block_till_done(wait_background_tasks=True)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
with patch(
"homeassistant.components.avea.config_flow.bluetooth.async_request_active_scan"
) as mock_request_active_scan:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
assert result["data_schema"].schema[CONF_ADDRESS].container == {
AVEA_DISCOVERY_INFO.address: (
f"{AVEA_DISCOVERY_INFO.name} ({AVEA_DISCOVERY_INFO.address})"
)
}
mock_request_active_scan.assert_awaited_once_with(hass)
with (
patch(
@@ -67,14 +80,62 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None:
inject_bluetooth_service_info(hass, NOT_AVEA_DISCOVERY_INFO)
await hass.async_block_till_done(wait_background_tasks=True)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
with patch(
"homeassistant.components.avea.config_flow.bluetooth.async_request_active_scan"
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_devices_found"
async def test_user_step_unnamed_device_label(hass: HomeAssistant) -> None:
"""Test unnamed discovered devices are shown without duplicating the address."""
discovery_info = type(AVEA_DISCOVERY_INFO)(
name=AVEA_DISCOVERY_INFO.address,
address=AVEA_DISCOVERY_INFO.address,
rssi=-60,
manufacturer_data={},
service_uuids=[AVEA_SERVICE_UUID],
service_data={},
source="local",
device=generate_ble_device(
address=AVEA_DISCOVERY_INFO.address, name=AVEA_DISCOVERY_INFO.address
),
advertisement=generate_advertisement_data(
local_name=AVEA_DISCOVERY_INFO.address,
manufacturer_data={},
service_data={},
service_uuids=[AVEA_SERVICE_UUID],
),
time=0,
connectable=True,
tx_power=-127,
)
with (
patch(
"homeassistant.components.avea.config_flow.async_discovered_service_info",
return_value=[discovery_info],
),
patch(
"homeassistant.components.avea.config_flow.bluetooth.async_request_active_scan"
) as mock_request_active_scan,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["data_schema"].schema[CONF_ADDRESS].container == {
AVEA_DISCOVERY_INFO.address: AVEA_DISCOVERY_INFO.address
}
mock_request_active_scan.assert_awaited_once_with(hass)
async def test_user_step_cannot_connect_recovers(hass: HomeAssistant) -> None:
"""Test the user step recovers after a cannot connect error."""
inject_bluetooth_service_info(hass, AVEA_DISCOVERY_INFO)
@@ -1488,11 +1488,6 @@
'infrared': False,
'matrix': False,
'max_kelvin': 9000,
'min_ext_mz_firmware': 1532997580,
'min_ext_mz_firmware_components': list([
2,
77,
]),
'min_kelvin': 1500,
'multizone': True,
'relays': False,
+20
View File
@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, patch
import aiohttp
from freezegun.api import FrozenDateTimeFactory
from loqedAPI import loqed
import pytest
from homeassistant.components.loqed.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
@@ -214,3 +215,22 @@ async def test_unload_entry_fails(
lock.deleteWebhook = AsyncMock(side_effect=Exception)
assert not await hass.config_entries.async_unload(integration.entry_id)
@pytest.mark.parametrize("error", [aiohttp.ClientError, TimeoutError])
async def test_unload_entry_with_unreachable_bridge(
hass: HomeAssistant,
integration: MockConfigEntry,
lock: loqed.Lock,
error: type[Exception],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test entry still unloads when the bridge is unreachable."""
lock.getWebhooks = AsyncMock(side_effect=error)
assert await hass.config_entries.async_unload(integration.entry_id)
await hass.async_block_till_done()
assert integration.state is ConfigEntryState.NOT_LOADED
assert not hass.data.get(DOMAIN)
assert "Could not remove webhook from LOQED bridge" in caplog.text
+47
View File
@@ -8,6 +8,10 @@ import pytest
from syrupy.assertion import SnapshotAssertion
from wled import Releases, WLEDError
from homeassistant.components.homeassistant import (
DOMAIN as HOME_ASSISTANT_DOMAIN,
SERVICE_UPDATE_ENTITY,
)
from homeassistant.components.update import (
ATTR_INSTALLED_VERSION,
ATTR_LATEST_VERSION,
@@ -25,6 +29,8 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.update_coordinator import REQUEST_REFRESH_DEFAULT_COOLDOWN
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@@ -186,3 +192,44 @@ async def test_update_stay_beta(
)
assert mock_wled.upgrade.call_count == 1
mock_wled.upgrade.assert_called_with(version="1.0.0b5")
async def test_update_entities(
hass: HomeAssistant,
mock_wled: MagicMock,
mock_wled_releases: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test update entity async_update method."""
await async_setup_component(hass, HOME_ASSISTANT_DOMAIN, {})
assert (state := hass.states.get("update.wled_rgb_light_firmware"))
assert state.state == STATE_ON
assert state.attributes[ATTR_LATEST_VERSION] == "0.99.0"
mock_wled_releases.releases.assert_called_once()
mock_wled_releases.releases.return_value = Releases(
beta="1.0.0b5",
nightly=None,
repo="wled/WLED",
stable="16.0.0",
)
await hass.services.async_call(
HOME_ASSISTANT_DOMAIN,
SERVICE_UPDATE_ENTITY,
{ATTR_ENTITY_ID: state.entity_id},
blocking=True,
)
# Ensure we pass the debouncer interval to allow async_request_refresh to execute
freezer.tick(REQUEST_REFRESH_DEFAULT_COOLDOWN)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# releases() should be called twice: once on setup, once for the manual update
assert mock_wled_releases.releases.call_count == 2
assert (state := hass.states.get("update.wled_rgb_light_firmware"))
assert state.state == STATE_ON
assert state.attributes[ATTR_LATEST_VERSION] == "16.0.0"