Compare commits

...

89 Commits

Author SHA1 Message Date
Franck Nijhof
f3b9bda876 2025.9.4 (#152634) 2025-09-19 23:08:58 +02:00
Yevhenii Vaskivskyi
3f3aaa2815 Bump asusrouter to 1.21.0 (#151607) 2025-09-19 20:38:05 +00:00
Franck Nijhof
6dc7870779 Bump version to 2025.9.4 2025-09-19 20:07:56 +00:00
G Johansson
be83416c72 Bump holidays to 0.81 (#152569) 2025-09-19 20:07:27 +00:00
G Johansson
c745ee18eb Bump holidays to 0.80 (#152306) 2025-09-19 20:07:25 +00:00
tronikos
cf907ae196 Bump opower to 0.15.5 (#152531) 2025-09-19 20:05:04 +00:00
Pete Sage
8eee53036a Fix Sonos set_volume float precision issue (#152493) 2025-09-19 20:00:42 +00:00
Alexandre CUER
b37237d24b Bump pyemoncms to 0.1.3 (#152436) 2025-09-19 20:00:41 +00:00
Matthias Alphart
950e758b62 Fix KNX UI schema missing DPT (#152430) 2025-09-19 20:00:40 +00:00
Josef Zweck
9cd940b7df Add La Marzocco specific client headers (#152419) 2025-09-19 20:00:38 +00:00
Josef Zweck
10b186a20d Bump pylamarzocco to 2.1.0 (#152364) 2025-09-19 20:00:37 +00:00
Imeon-Energy
757aec1c6b Bump imeon_inverter_api to 0.4.0 (#152351)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-09-19 20:00:36 +00:00
Allen Porter
0b159bdb9c Update authorization server to prefer absolute urls (#152313)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-19 20:00:34 +00:00
J. Nick Koston
8728312e87 Bump aiohomekit to 3.2.17 (#152297) 2025-09-19 20:00:33 +00:00
Åke Strandberg
bbb67db354 Add proper error handling for /actions endpoint for miele (#152290) 2025-09-19 20:00:31 +00:00
J. Nick Koston
265f5da21a Bump bluetooth-auto-recovery to 1.5.3 (#152256) 2025-09-19 20:00:30 +00:00
J. Nick Koston
54859e8a83 Bump aiohomekit to 3.2.16 (#152255) 2025-09-19 20:00:28 +00:00
Sean Dague
c87dba878d Upgrade waterfurnace to 1.2.0 (#152241) 2025-09-19 20:00:27 +00:00
J. Nick Koston
8d8e008123 Fix HomeKit Controller overwhelming resource-limited devices by batching characteristic polling (#152209) 2025-09-19 20:00:25 +00:00
Yevhenii Vaskivskyi
b30667a469 Fix bug with the hardcoded configuration_url (asuswrt) (#151858) 2025-09-19 20:00:23 +00:00
Manu
8920c548d5 Bump habiticalib to v0.4.5 (#151720) 2025-09-19 20:00:22 +00:00
Manu
eac719f9af Bump habiticalib to v0.4.4 (#151332) 2025-09-19 20:00:20 +00:00
Paulus Schoutsen
3499ed7a98 2025.9.3 (#152237) 2025-09-13 08:44:08 -04:00
Paulus Schoutsen
2c809d5903 Bump version to 2025.9.3 2025-09-13 12:14:15 +00:00
J. Nick Koston
40988198f3 Bump habluetooth to 5.6.4 (#152227) 2025-09-13 12:14:08 +00:00
Franck Nijhof
ab5d1d27f1 2025.9.2 (#152198) 2025-09-12 23:10:27 +02:00
wollew
1c10b85fed Use position percentage for closed status in Velux (#151679)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-12 20:39:06 +00:00
Franck Nijhof
91a7db08ff Bump version to 2025.9.2 2025-09-12 20:20:56 +00:00
Bram Kragten
a764d54123 Update frontend to 20250903.5 (#152170) 2025-09-12 20:20:44 +00:00
Joakim Sørensen
dc09e33556 Bump hass-nabucasa from 1.1.0 to 1.1.1 (#152147) 2025-09-12 20:20:43 +00:00
Simone Chemelli
14173bd9ec Fix reauth for Alexa Devices (#152128) 2025-09-12 20:20:42 +00:00
Jan Bouwhuis
d2e7537629 Fix supported _color_modes attribute not set for on/off MQTT JSON light (#152126) 2025-09-12 20:20:41 +00:00
J. Nick Koston
9a165a64fe Fix DoorBird being updated with wrong IP addresses during discovery (#152088)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-12 20:20:39 +00:00
Abílio Costa
9c749a6abc Fix duplicated IP port usage in Govee Light Local (#152087) 2025-09-12 20:20:38 +00:00
J. Nick Koston
2e33222c71 Fix HomeKit Controller stale values at startup (#152086)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-09-12 20:20:37 +00:00
Maciej Bieniek
ab1c2c4f70 Bump accuweather to version 4.2.1 (#152029) 2025-09-12 20:20:36 +00:00
Joost Lekkerkerker
529219ae69 Bump yt-dlp to 2025.09.05 (#152006) 2025-09-12 20:20:35 +00:00
J. Nick Koston
d6ce71fa61 Bump habluetooth to 5.6.2 (#151985) 2025-09-12 20:20:33 +00:00
peteS-UK
e5b67d513a Fix playlist media_class_filter in search_media for squeezebox (#151973) 2025-09-12 20:20:32 +00:00
peteS-UK
a547179f66 Fix for squeezebox track content_type (#151963) 2025-09-12 20:20:31 +00:00
epenet
8c61788a7d Fix invalid logger in Tuya (#151957) 2025-09-12 20:19:26 +00:00
J. Nick Koston
6b934d94db Bump habluetooth to 5.6.0 (#151942) 2025-09-12 20:18:38 +00:00
J. Nick Koston
d30ad82774 Bump bleak-esphome to 3.3.0 (#151922)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-09-12 20:18:37 +00:00
J. Nick Koston
4618b33e93 Bump habluetooth to 5.5.1 (#151921) 2025-09-12 20:18:36 +00:00
epenet
d6299094db Fix _is_valid_suggested_unit in sensor platform (#151912) 2025-09-12 20:18:35 +00:00
Simone Chemelli
087d9d30c0 Avoid cleanup/recreate of device_trackers not linked to a device for Vodafone Station (#151904) 2025-09-12 20:18:33 +00:00
Simone Chemelli
f07890cf5c Bump aiovodafone to 1.2.1 (#151901) 2025-09-12 20:18:31 +00:00
Avi Miller
e5b78cc481 Bump aiolifx-themes to 1.0.2 to support newer LIFX devices (#151898)
Signed-off-by: Avi Miller <me@dje.li>
2025-09-12 20:18:30 +00:00
Manu
12b409d8e1 Bump aiontfy to v0.5.5 (#151869) 2025-09-12 20:18:29 +00:00
Maciej Bieniek
def5408db8 Use native_visibility property instead of visibility for OpenWeatherMap weather entity (#151867) 2025-09-12 20:18:28 +00:00
Robert Resch
f105b45ee2 Bump aioecowitt to 2025.9.1 (#151859) 2025-09-12 20:18:26 +00:00
wollew
9d904c30a7 fix rain sensor for Velux GPU windows (#151857) 2025-09-12 20:18:25 +00:00
Martins Sipenko
99b047939f Update pysmarty2 to 0.10.3 (#151855) 2025-09-12 20:18:24 +00:00
J. Nick Koston
3a615908ee Bump aioharmony to 0.5.3 (#151853) 2025-09-12 20:18:23 +00:00
David Knowles
baff541f46 Bump pydrawise to 2025.9.0 (#151842) 2025-09-12 20:18:22 +00:00
jan iversen
6d8c35cfe9 removed assert fron entity in modbus. (#151834) 2025-09-12 20:18:20 +00:00
jan iversen
b8d9883e74 max_temp / min_temp in modbus light could only be int, otherwise an assert was provoked. (#151833) 2025-09-12 20:18:19 +00:00
jan iversen
c3c65af450 Allow delay > 1 in modbus. (#151832) 2025-09-12 20:18:18 +00:00
Maciej Bieniek
3af8616764 Mark Tractive switches as unavailable when tacker is in the enegy saving zone (#151817) 2025-09-12 20:18:17 +00:00
Matthias Alphart
64ec4609c5 Fix KNX Light - individual color initialisation from UI config (#151815) 2025-09-12 20:18:15 +00:00
Matthias Alphart
c78bc26b83 Fix KNX BinarySensor config_store data (#151808) 2025-09-12 20:18:14 +00:00
J. Nick Koston
0c093646c9 Bump habluetooth to 5.3.1 (#151803) 2025-09-12 20:18:13 +00:00
Simone Chemelli
1b27acdde0 Improve config entry migration for edge cases in Alexa Devices (#151788) 2025-09-12 20:18:12 +00:00
Ludovic BOUÉ
9dafc0e02f Remove device class for Matter NitrogenDioxideSensor (#151782)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-12 20:18:10 +00:00
Tsvi Mostovicz
0091dafcb0 Revert "Jewish Calendar add coordinator " (#151780) 2025-09-12 20:18:09 +00:00
Jan Bouwhuis
b387acffb7 Fix update of the entity ID does not clean up an old restored state (#151696)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-12 20:18:08 +00:00
blotus
36b3133fa2 Fix support for Ecowitt soil moisture sensors (#151685) 2025-09-12 20:18:06 +00:00
hbludworth
fe01e96012 Fix Aladdin Connect state not updating (#151652)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-12 20:18:05 +00:00
Thomas55555
0b56ec16ed Add event entity on websocket ready in Husqvarna Automower (#151428) 2025-09-12 20:18:04 +00:00
Mark Adkins
ca79f4c963 Update SharkIQ authentication method (#151046) 2025-09-12 20:18:02 +00:00
Bob Igo
9a43f2776d Fix XMPP not working with non-TLS servers (#150957) 2025-09-12 20:18:01 +00:00
Franck Nijhof
0cda883b56 2025.9.1 (#151766) 2025-09-05 13:13:34 +02:00
Franck Nijhof
ae58e633f0 Bump version to 2025.9.1 2025-09-05 10:33:36 +00:00
jan iversen
06480bfd9d Fix enable/disable entity in modbus (#151626) 2025-09-05 10:33:04 +00:00
Artur Pragacz
625f586945 Fix recognition of entity names in default agent with interpunction (#151759) 2025-09-05 10:30:27 +00:00
Richard Kroegel
7dbeaa475d Bump bimmer_connected to 0.17.3 (#151756) 2025-09-05 10:30:24 +00:00
David Knowles
dff3d5f8af Bump pyschlage to 2025.9.0 (#151731) 2025-09-05 10:30:21 +00:00
Michael Hansen
89c335919a Handle match failures in intent HTTP API (#151726) 2025-09-05 10:30:16 +00:00
Daniel Hjelseth Høyer
2bb4573357 Update Mill library 0.13.1 (#151712) 2025-09-05 10:30:01 +00:00
Dan Raper
7037ce989c Bump ohmepy version to 1.5.2 (#151707) 2025-09-05 10:29:58 +00:00
Dan Raper
bfdd2053ba Require OhmeAdvancedSettingsCoordinator to run regardless of entities (#151701) 2025-09-05 10:29:55 +00:00
Bram Kragten
fcc3f92f8c Update frontend to 20250903.3 (#151694) 2025-09-05 10:29:51 +00:00
Marcel van der Veldt
8710267d53 Bump aiohue to 4.7.5 (#151684) 2025-09-05 10:29:48 +00:00
Imeon-Energy
85b6adcc9a Fix, entities stay unavailable after timeout error, Imeon inverter integration (#151671)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-09-05 10:29:46 +00:00
Felipe Santos
beec6e86e0 Fix WebSocket proxy for add-ons not forwarding ping/pong frame data (#151654) 2025-09-05 10:29:45 +00:00
Pete Sage
3dacffaaf9 Fix Sonos Dialog Select type conversion (#151649) 2025-09-05 10:29:41 +00:00
Manu
d90f2a1de1 Correct capitalization of "FRITZ!Box" in FRITZ!Box Tools integration (#151637) 2025-09-05 10:29:39 +00:00
karwosts
b6c9217429 Add missing device trigger duration localizations (#151578)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-09-05 10:29:37 +00:00
Marcel van der Veldt
7fc8da6769 Add support for migrated Hue bridge (#151411)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-05 10:29:34 +00:00
146 changed files with 2289 additions and 688 deletions

4
CODEOWNERS generated
View File

@@ -1690,8 +1690,8 @@ build.json @home-assistant/supervisor
/tests/components/vegehub/ @ghowevege
/homeassistant/components/velbus/ @Cereal2nd @brefra
/tests/components/velbus/ @Cereal2nd @brefra
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
/homeassistant/components/venstar/ @garbled1 @jhollowe
/tests/components/venstar/ @garbled1 @jhollowe
/homeassistant/components/versasense/ @imstevenxyz

View File

@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["accuweather"],
"requirements": ["accuweather==4.2.0"],
"requirements": ["accuweather==4.2.1"],
"single_config_entry": true
}

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from genie_partner_sdk.client import AladdinConnectClient
from genie_partner_sdk.model import GarageDoor
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
@@ -36,22 +35,7 @@ async def async_setup_entry(
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
)
sdk_doors = await client.get_doors()
# Convert SDK GarageDoor objects to integration GarageDoor objects
doors = [
GarageDoor(
{
"device_id": door.device_id,
"door_number": door.door_number,
"name": door.name,
"status": door.status,
"link_status": door.link_status,
"battery_level": door.battery_level,
}
)
for door in sdk_doors
]
doors = await client.get_doors()
entry.runtime_data = {
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)

View File

@@ -41,4 +41,10 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
async def _async_update_data(self) -> GarageDoor:
"""Fetch data from the Aladdin Connect API."""
await self.client.update_door(self.data.device_id, self.data.door_number)
self.data.status = self.client.get_door_status(
self.data.device_id, self.data.door_number
)
self.data.battery_level = self.client.get_battery_status(
self.data.device_id, self.data.door_number
)
return self.data

View File

@@ -49,7 +49,9 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
@property
def is_closed(self) -> bool | None:
"""Update is closed attribute."""
return self.coordinator.data.status == "closed"
if (status := self.coordinator.data.status) is None:
return None
return status == "closed"
@property
def is_closing(self) -> bool | None:

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["genie-partner-sdk==1.0.10"]
"requirements": ["genie-partner-sdk==1.0.11"]
}

View File

@@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import _LOGGER, CONF_LOGIN_DATA, COUNTRY_DOMAINS, DOMAIN
from .const import _LOGGER, CONF_LOGIN_DATA, CONF_SITE, COUNTRY_DOMAINS, DOMAIN
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .services import async_setup_services
@@ -42,7 +42,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Migrate old entry."""
if entry.version == 1 and entry.minor_version == 1:
if entry.version == 1 and entry.minor_version < 3:
if CONF_SITE in entry.data:
# Site in data (wrong place), just move to login data
new_data = entry.data.copy()
new_data[CONF_LOGIN_DATA][CONF_SITE] = new_data[CONF_SITE]
new_data.pop(CONF_SITE)
hass.config_entries.async_update_entry(
entry, data=new_data, version=1, minor_version=3
)
return True
if CONF_SITE in entry.data[CONF_LOGIN_DATA]:
# Site is there, just update version to avoid future migrations
hass.config_entries.async_update_entry(entry, version=1, minor_version=3)
return True
_LOGGER.debug(
"Migrating from version %s.%s", entry.version, entry.minor_version
)
@@ -53,10 +69,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) ->
# Add site to login data
new_data = entry.data.copy()
new_data[CONF_LOGIN_DATA]["site"] = f"https://www.amazon.{domain}"
new_data[CONF_LOGIN_DATA][CONF_SITE] = f"https://www.amazon.{domain}"
hass.config_entries.async_update_entry(
entry, data=new_data, version=1, minor_version=2
entry, data=new_data, version=1, minor_version=3
)
_LOGGER.info(

View File

@@ -52,7 +52,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Alexa Devices."""
VERSION = 1
MINOR_VERSION = 2
MINOR_VERSION = 3
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -107,7 +107,9 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
await validate_input(self.hass, {**reauth_entry.data, **user_input})
data = await validate_input(
self.hass, {**reauth_entry.data, **user_input}
)
except CannotConnect:
errors["base"] = "cannot_connect"
except (CannotAuthenticate, TypeError):
@@ -119,8 +121,9 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
reauth_entry,
data={
CONF_USERNAME: entry_data[CONF_USERNAME],
CONF_PASSWORD: entry_data[CONF_PASSWORD],
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_CODE: user_input[CONF_CODE],
CONF_LOGIN_DATA: data,
},
)

View File

@@ -6,6 +6,7 @@ _LOGGER = logging.getLogger(__package__)
DOMAIN = "alexa_devices"
CONF_LOGIN_DATA = "login_data"
CONF_SITE = "site"
DEFAULT_DOMAIN = "com"
COUNTRY_DOMAINS = {

View File

@@ -120,11 +120,17 @@ class AsusWrtBridge(ABC):
def __init__(self, host: str) -> None:
"""Initialize Bridge."""
self._configuration_url = f"http://{host}"
self._host = host
self._firmware: str | None = None
self._label_mac: str | None = None
self._model: str | None = None
@property
def configuration_url(self) -> str:
"""Return configuration URL."""
return self._configuration_url
@property
def host(self) -> str:
"""Return hostname."""
@@ -359,6 +365,7 @@ class AsusWrtHttpBridge(AsusWrtBridge):
# get main router properties
if mac := _identity.mac:
self._label_mac = format_mac(mac)
self._configuration_url = self._api.webpanel
self._firmware = str(_identity.firmware)
self._model = _identity.model

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
"requirements": ["aioasuswrt==1.4.0", "asusrouter==1.20.1"]
"requirements": ["aioasuswrt==1.4.0", "asusrouter==1.21.0"]
}

View File

@@ -388,11 +388,11 @@ class AsusWrtRouter:
def device_info(self) -> DeviceInfo:
"""Return the device information."""
info = DeviceInfo(
configuration_url=self._api.configuration_url,
identifiers={(DOMAIN, self._entry.unique_id or "AsusWRT")},
name=self.host,
model=self._api.model or "Asus Router",
manufacturer="Asus",
configuration_url=f"http://{self.host}",
)
if self._api.firmware:
info["sw_version"] = self._api.firmware

View File

@@ -92,7 +92,11 @@ from homeassistant.components.http.ban import (
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.network import is_cloud_connection
from homeassistant.helpers.network import (
NoURLAvailableError,
get_url,
is_cloud_connection,
)
from homeassistant.util.network import is_local
from . import indieauth
@@ -125,11 +129,18 @@ class WellKnownOAuthInfoView(HomeAssistantView):
async def get(self, request: web.Request) -> web.Response:
"""Return the well known OAuth2 authorization info."""
hass = request.app[KEY_HASS]
# Some applications require absolute urls, so we prefer using the
# current requests url if possible, with fallback to a relative url.
try:
url_prefix = get_url(hass, require_current_request=True)
except NoURLAvailableError:
url_prefix = ""
return self.json(
{
"authorization_endpoint": "/auth/authorize",
"token_endpoint": "/auth/token",
"revocation_endpoint": "/auth/revoke",
"authorization_endpoint": f"{url_prefix}/auth/authorize",
"token_endpoint": f"{url_prefix}/auth/token",
"revocation_endpoint": f"{url_prefix}/auth/revoke",
"response_types_supported": ["code"],
"service_documentation": (
"https://developers.home-assistant.io/docs/auth_api"

View File

@@ -18,9 +18,9 @@
"bleak==1.0.1",
"bleak-retry-connector==4.4.3",
"bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.2",
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.28.2",
"dbus-fast==2.44.3",
"habluetooth==5.3.0"
"habluetooth==5.6.4"
]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"iot_class": "cloud_polling",
"loggers": ["bimmer_connected"],
"requirements": ["bimmer-connected[china]==0.17.2"]
"requirements": ["bimmer-connected[china]==0.17.3"]
}

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.1.0"],
"requirements": ["hass-nabucasa==1.1.1"],
"single_config_entry": true
}

View File

@@ -35,7 +35,7 @@ from hassil.recognize import (
)
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
from hassil.trie import Trie
from hassil.util import merge_dict
from hassil.util import merge_dict, remove_punctuation
from home_assistant_intents import (
ErrorKey,
FuzzyConfig,
@@ -327,12 +327,10 @@ class DefaultAgent(ConversationEntity):
if self._exposed_names_trie is not None:
# Filter by input string
text_lower = user_input.text.strip().lower()
text = remove_punctuation(user_input.text).strip().lower()
slot_lists["name"] = TextSlotList(
name="name",
values=[
result[2] for result in self._exposed_names_trie.find(text_lower)
],
values=[result[2] for result in self._exposed_names_trie.find(text)],
)
start = time.monotonic()
@@ -1263,7 +1261,7 @@ class DefaultAgent(ConversationEntity):
name_list = TextSlotList.from_tuples(exposed_entity_names, allow_template=False)
for name_value in name_list.values:
assert isinstance(name_value.text_in, TextChunk)
name_text = name_value.text_in.text.strip().lower()
name_text = remove_punctuation(name_value.text_in.text).strip().lower()
self._exposed_names_trie.insert(name_text, name_value)
self._slot_lists = {

View File

@@ -19,8 +19,10 @@ from homeassistant.config_entries import (
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from homeassistant.helpers.typing import VolDictType
@@ -103,6 +105,43 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN):
"""Initialize the DoorBird config flow."""
self.discovery_schema: vol.Schema | None = None
async def _async_verify_existing_device_for_discovery(
self,
existing_entry: ConfigEntry,
host: str,
macaddress: str,
) -> None:
"""Verify discovered device matches existing entry before updating IP.
This method performs the following verification steps:
1. Ensures that the stored credentials work before updating the entry.
2. Verifies that the device at the discovered IP address has the expected MAC address.
"""
info, errors = await self._async_validate_or_error(
{
**existing_entry.data,
CONF_HOST: host,
}
)
if errors:
_LOGGER.debug(
"Cannot validate DoorBird at %s with existing credentials: %s",
host,
errors,
)
raise AbortFlow("cannot_connect")
# Verify the MAC address matches what was advertised
if format_mac(info["mac_addr"]) != format_mac(macaddress):
_LOGGER.debug(
"DoorBird at %s reports MAC %s but zeroconf advertised %s, ignoring",
host,
info["mac_addr"],
macaddress,
)
raise AbortFlow("wrong_device")
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
@@ -172,7 +211,22 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(macaddress)
host = discovery_info.host
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
# Check if we have an existing entry for this MAC
existing_entry = self.hass.config_entries.async_entry_for_domain_unique_id(
DOMAIN, macaddress
)
if existing_entry:
# Check if the host is actually changing
if existing_entry.data.get(CONF_HOST) != host:
await self._async_verify_existing_device_for_discovery(
existing_entry, host, macaddress
)
# All checks passed or no change needed, abort
# if already configured with potential IP update
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
self._async_abort_entries_match({CONF_HOST: host})

View File

@@ -49,6 +49,8 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"link_local_address": "Link local addresses are not supported",
"not_doorbird_device": "This device is not a DoorBird",
"not_ipv4_address": "Only IPv4 addresses are supported",
"wrong_device": "Device MAC address does not match",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"flow_title": "{name} ({host})",

View File

@@ -6,5 +6,5 @@
"dependencies": ["webhook"],
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
"iot_class": "local_push",
"requirements": ["aioecowitt==2025.9.0"]
"requirements": ["aioecowitt==2025.9.1"]
}

View File

@@ -218,6 +218,12 @@ ECOWITT_SENSORS_MAPPING: Final = {
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.SOIL_MOISTURE: SensorEntityDescription(
key="SOIL_MOISTURE",
device_class=SensorDeviceClass.MOISTURE,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/emoncms",
"iot_class": "local_polling",
"requirements": ["pyemoncms==0.1.2"]
"requirements": ["pyemoncms==0.1.3"]
}

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/emoncms_history",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["pyemoncms==0.1.2"]
"requirements": ["pyemoncms==0.1.3"]
}

View File

@@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==2.1.0", "bleak-esphome==3.2.0"]
"requirements": ["eq3btsmart==2.1.0", "bleak-esphome==3.3.0"]
}

View File

@@ -19,7 +19,7 @@
"requirements": [
"aioesphomeapi==39.0.1",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.2.0"
"bleak-esphome==3.3.0"
],
"zeroconf": ["_esphomelib._tcp.local."]
}

View File

@@ -14,6 +14,9 @@
"toggle": "[%key:common::device_automation::action_type::toggle%]",
"turn_on": "[%key:common::device_automation::action_type::turn_on%]",
"turn_off": "[%key:common::device_automation::action_type::turn_off%]"
},
"extra_fields": {
"for": "[%key:common::device_automation::extra_fields::for%]"
}
},
"entity_component": {

View File

@@ -183,8 +183,8 @@
"description": "Sets a new password for the guest Wi-Fi. The password must be between 8 and 63 characters long. If no additional parameter is set, the password will be auto-generated with a length of 12 characters.",
"fields": {
"device_id": {
"name": "Fritz!Box Device",
"description": "Select the Fritz!Box to configure."
"name": "FRITZ!Box Device",
"description": "Select the FRITZ!Box to configure."
},
"password": {
"name": "[%key:common::config_flow::data::password%]",

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250903.2"]
"requirements": ["home-assistant-frontend==20250903.5"]
}

View File

@@ -26,16 +26,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -> bool:
"""Set up Govee light local from a config entry."""
# Get source IPs for all enabled adapters
source_ips = await network.async_get_enabled_source_ips(hass)
source_ips = await async_get_source_ips(hass)
_LOGGER.debug("Enabled source IPs: %s", source_ips)
coordinator: GoveeLocalApiCoordinator = GoveeLocalApiCoordinator(
hass=hass,
config_entry=entry,
source_ips=[
source_ip for source_ip in source_ips if isinstance(source_ip, IPv4Address)
],
hass=hass, config_entry=entry, source_ips=source_ips
)
async def await_cleanup():
@@ -76,3 +71,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -
async def async_unload_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_get_source_ips(
hass: HomeAssistant,
) -> set[str]:
"""Get the source ips for Govee local."""
source_ips = await network.async_get_enabled_source_ips(hass)
return {
str(source_ip) for source_ip in source_ips if isinstance(source_ip, IPv4Address)
}

View File

@@ -4,15 +4,14 @@ from __future__ import annotations
import asyncio
from contextlib import suppress
from ipaddress import IPv4Address
import logging
from govee_local_api import GoveeController
from homeassistant.components import network
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_flow
from . import async_get_source_ips
from .const import (
CONF_LISTENING_PORT_DEFAULT,
CONF_MULTICAST_ADDRESS_DEFAULT,
@@ -24,11 +23,11 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
async def _async_discover(hass: HomeAssistant, adapter_ip: IPv4Address) -> bool:
async def _async_discover(hass: HomeAssistant, adapter_ip: str) -> bool:
controller: GoveeController = GoveeController(
loop=hass.loop,
logger=_LOGGER,
listening_address=str(adapter_ip),
listening_address=adapter_ip,
broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT,
broadcast_port=CONF_TARGET_PORT_DEFAULT,
listening_port=CONF_LISTENING_PORT_DEFAULT,
@@ -62,14 +61,8 @@ async def _async_discover(hass: HomeAssistant, adapter_ip: IPv4Address) -> bool:
async def _async_has_devices(hass: HomeAssistant) -> bool:
"""Return if there are devices that can be discovered."""
# Get source IPs for all enabled adapters
source_ips = await network.async_get_enabled_source_ips(hass)
_LOGGER.debug("Enabled source IPs: %s", source_ips)
# Run discovery on every IPv4 address and gather results
results = await asyncio.gather(
*[_async_discover(hass, ip) for ip in source_ips if isinstance(ip, IPv4Address)]
)
source_ips = await async_get_source_ips(hass)
results = await asyncio.gather(*[_async_discover(hass, ip) for ip in source_ips])
return any(results)

View File

@@ -2,7 +2,6 @@
import asyncio
from collections.abc import Callable
from ipaddress import IPv4Address
import logging
from govee_local_api import GoveeController, GoveeDevice
@@ -30,7 +29,7 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]):
self,
hass: HomeAssistant,
config_entry: GoveeLocalConfigEntry,
source_ips: list[IPv4Address],
source_ips: set[str],
) -> None:
"""Initialize my coordinator."""
super().__init__(
@@ -45,7 +44,7 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]):
GoveeController(
loop=hass.loop,
logger=_LOGGER,
listening_address=str(source_ip),
listening_address=source_ip,
broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT,
broadcast_port=CONF_TARGET_PORT_DEFAULT,
listening_port=CONF_LISTENING_PORT_DEFAULT,

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["habiticalib"],
"quality_scale": "platinum",
"requirements": ["habiticalib==0.4.3"]
"requirements": ["habiticalib==0.4.5"]
}

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/harmony",
"iot_class": "local_push",
"loggers": ["aioharmony", "slixmpp"],
"requirements": ["aioharmony==0.5.2"],
"requirements": ["aioharmony==0.5.3"],
"ssdp": [
{
"manufacturer": "Logitech",

View File

@@ -303,9 +303,9 @@ async def _websocket_forward(
elif msg.type is aiohttp.WSMsgType.BINARY:
await ws_to.send_bytes(msg.data)
elif msg.type is aiohttp.WSMsgType.PING:
await ws_to.ping()
await ws_to.ping(msg.data)
elif msg.type is aiohttp.WSMsgType.PONG:
await ws_to.pong()
await ws_to.pong(msg.data)
elif ws_to.closed:
await ws_to.close(code=ws_to.close_code, message=msg.extra) # type: ignore[arg-type]
except RuntimeError:

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.79", "babel==2.15.0"]
"requirements": ["holidays==0.81", "babel==2.15.0"]
}

View File

@@ -20,7 +20,12 @@ from aiohomekit.exceptions import (
EncryptionError,
)
from aiohomekit.model import Accessories, Accessory, Transport
from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes
from aiohomekit.model.characteristics import (
EVENT_CHARACTERISTICS,
Characteristic,
CharacteristicPermissions,
CharacteristicsTypes,
)
from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.thread import async_get_preferred_dataset
@@ -52,7 +57,10 @@ from .utils import IidTuple, unique_id_to_iids
RETRY_INTERVAL = 60 # seconds
MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3
# HomeKit accessories have varying limits on how many characteristics
# they can handle per request. Since we don't know each device's specific limit,
# we batch requests to a conservative size to avoid overwhelming any device.
MAX_CHARACTERISTICS_PER_REQUEST = 49
BLE_AVAILABILITY_CHECK_INTERVAL = 1800 # seconds
@@ -179,6 +187,21 @@ class HKDevice:
for aid_iid in characteristics:
self.pollable_characteristics.discard(aid_iid)
def get_all_pollable_characteristics(self) -> set[tuple[int, int]]:
"""Get all characteristics that can be polled.
This is used during startup to poll all readable characteristics
before entities have registered what they care about.
"""
return {
(accessory.aid, char.iid)
for accessory in self.entity_map.accessories
for service in accessory.services
for char in service.characteristics
if CharacteristicPermissions.paired_read in char.perms
and char.type not in EVENT_CHARACTERISTICS
}
def add_watchable_characteristics(
self, characteristics: list[tuple[int, int]]
) -> None:
@@ -306,12 +329,20 @@ class HKDevice:
)
entry.async_on_unload(self._async_cancel_subscription_timer)
if transport != Transport.BLE:
# Although async_populate_accessories_state fetched the accessory database,
# the /accessories endpoint may return cached values from the accessory's
# perspective. For example, Ecobee thermostats may report stale temperature
# values (like 100°C) in their /accessories response after restarting.
# We need to explicitly poll characteristics to get fresh sensor readings
# before processing the entity map and creating devices.
# Use poll_all=True since entities haven't registered their characteristics yet.
await self.async_update(poll_all=True)
await self.async_process_entity_map()
if transport != Transport.BLE:
# Do a single poll to make sure the chars are
# up to date so we don't restore old data.
await self.async_update()
# Start regular polling after entity map is processed
self._async_start_polling()
# If everything is up to date, we can create the entities
@@ -863,9 +894,25 @@ class HKDevice:
"""Request an debounced update from the accessory."""
await self._debounced_update.async_call()
async def async_update(self, now: datetime | None = None) -> None:
"""Poll state of all entities attached to this bridge/accessory."""
to_poll = self.pollable_characteristics
async def async_update(
self, now: datetime | None = None, *, poll_all: bool = False
) -> None:
"""Poll state of all entities attached to this bridge/accessory.
Args:
now: The current time (used by time interval callbacks).
poll_all: If True, poll all readable characteristics instead
of just the registered ones.
This is useful during initial setup before entities have
registered their characteristics.
"""
if poll_all:
# Poll all readable characteristics during initial startup
# excluding device trigger characteristics (buttons, doorbell, etc.)
to_poll = self.get_all_pollable_characteristics()
else:
to_poll = self.pollable_characteristics
if not to_poll:
self.async_update_available_state()
_LOGGER.debug(
@@ -898,20 +945,26 @@ class HKDevice:
async with self._polling_lock:
_LOGGER.debug("Starting HomeKit device update: %s", self.unique_id)
try:
new_values_dict = await self.get_characteristics(to_poll)
except AccessoryNotFoundError:
# Not only did the connection fail, but also the accessory is not
# visible on the network.
self.async_set_available_state(False)
return
except (AccessoryDisconnectedError, EncryptionError):
# Temporary connection failure. Device may still available but our
# connection was dropped or we are reconnecting
self._poll_failures += 1
if self._poll_failures >= MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE:
new_values_dict: dict[tuple[int, int], dict[str, Any]] = {}
to_poll_list = list(to_poll)
for i in range(0, len(to_poll_list), MAX_CHARACTERISTICS_PER_REQUEST):
batch = to_poll_list[i : i + MAX_CHARACTERISTICS_PER_REQUEST]
try:
batch_values = await self.get_characteristics(batch)
new_values_dict.update(batch_values)
except AccessoryNotFoundError:
# Not only did the connection fail, but also the accessory is not
# visible on the network.
self.async_set_available_state(False)
return
return
except (AccessoryDisconnectedError, EncryptionError):
# Temporary connection failure. Device may still available but our
# connection was dropped or we are reconnecting
self._poll_failures += 1
if self._poll_failures >= MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE:
self.async_set_available_state(False)
return
self._poll_failures = 0
self.process_new_events(new_values_dict)

View File

@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.2.15"],
"requirements": ["aiohomekit==3.2.17"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}

View File

@@ -9,7 +9,9 @@ from typing import Any
import aiohttp
from aiohue import LinkButtonNotPressed, create_app_key
from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp
from aiohue.errors import AiohueException
from aiohue.util import normalize_bridge_id
from aiohue.v2 import HueBridgeV2
import slugify as unicode_slug
import voluptuous as vol
@@ -40,6 +42,9 @@ HUE_MANUFACTURERURL = ("http://www.philips.com", "http://www.philips-hue.com")
HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"]
HUE_MANUAL_BRIDGE_ID = "manual"
BSB002_MODEL_ID = "BSB002"
BSB003_MODEL_ID = "BSB003"
class HueFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Hue config flow."""
@@ -74,7 +79,14 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
"""Return a DiscoveredHueBridge object."""
try:
bridge = await discover_bridge(
host, websession=aiohttp_client.async_get_clientsession(self.hass)
host,
websession=aiohttp_client.async_get_clientsession(
# NOTE: we disable SSL verification for now due to the fact that the (BSB003)
# Hue bridge uses a certificate from a on-bridge root authority.
# We need to specifically handle this case in a follow-up update.
self.hass,
verify_ssl=False,
),
)
except aiohttp.ClientError as err:
LOGGER.warning(
@@ -110,7 +122,9 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
try:
async with asyncio.timeout(5):
bridges = await discover_nupnp(
websession=aiohttp_client.async_get_clientsession(self.hass)
websession=aiohttp_client.async_get_clientsession(
self.hass, verify_ssl=False
)
)
except TimeoutError:
bridges = []
@@ -178,7 +192,9 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
app_key = await create_app_key(
bridge.host,
f"home-assistant#{device_name}",
websession=aiohttp_client.async_get_clientsession(self.hass),
websession=aiohttp_client.async_get_clientsession(
self.hass, verify_ssl=False
),
)
except LinkButtonNotPressed:
errors["base"] = "register_failed"
@@ -228,7 +244,6 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured(
updates={CONF_HOST: discovery_info.host}, reload_on_update=True
)
# we need to query the other capabilities too
bridge = await self._get_bridge(
discovery_info.host, discovery_info.properties["bridgeid"]
@@ -236,6 +251,14 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
if bridge is None:
return self.async_abort(reason="cannot_connect")
self.bridge = bridge
if (
bridge.supports_v2
and discovery_info.properties.get("modelid") == BSB003_MODEL_ID
):
# try to handle migration of BSB002 --> BSB003
if await self._check_migrated_bridge(bridge):
return self.async_abort(reason="migrated_bridge")
return await self.async_step_link()
async def async_step_homekit(
@@ -272,6 +295,55 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
self.bridge = bridge
return await self.async_step_link()
async def _check_migrated_bridge(self, bridge: DiscoveredHueBridge) -> bool:
"""Check if the discovered bridge is a migrated bridge."""
# Try to handle migration of BSB002 --> BSB003.
# Once we detect a BSB003 bridge on the network which has not yet been
# configured in HA (otherwise we would have had a unique id match),
# we check if we have any existing (BSB002) entries and if we can connect to the
# new bridge with our previously stored api key.
# If that succeeds, we migrate the entry to the new bridge.
for conf_entry in self.hass.config_entries.async_entries(
DOMAIN, include_ignore=False, include_disabled=False
):
if conf_entry.data[CONF_API_VERSION] != 2:
continue
if conf_entry.data[CONF_HOST] == bridge.host:
continue
# found an existing (BSB002) bridge entry,
# check if we can connect to the new BSB003 bridge using the old credentials
api = HueBridgeV2(bridge.host, conf_entry.data[CONF_API_KEY])
try:
await api.fetch_full_state()
except (AiohueException, aiohttp.ClientError):
continue
old_bridge_id = conf_entry.unique_id
assert old_bridge_id is not None
# found a matching entry, migrate it
self.hass.config_entries.async_update_entry(
conf_entry,
data={
**conf_entry.data,
CONF_HOST: bridge.host,
},
unique_id=bridge.id,
)
# also update the bridge device
dev_reg = dr.async_get(self.hass)
if bridge_device := dev_reg.async_get_device(
identifiers={(DOMAIN, old_bridge_id)}
):
dev_reg.async_update_device(
bridge_device.id,
# overwrite identifiers with new bridge id
new_identifiers={(DOMAIN, bridge.id)},
# overwrite mac addresses with empty set to drop the old (incorrect) addresses
# this will be auto corrected once the integration is loaded
new_connections=set(),
)
return True
return False
class HueV1OptionsFlowHandler(OptionsFlow):
"""Handle Hue options for V1 implementation."""

View File

@@ -10,6 +10,6 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["aiohue"],
"requirements": ["aiohue==4.7.4"],
"requirements": ["aiohue==4.7.5"],
"zeroconf": ["_hue._tcp.local."]
}

View File

@@ -36,12 +36,13 @@ async def async_setup_entry(
"""Set up Automower message event entities.
Entities are created dynamically based on messages received from the API,
but only for mowers that support message events.
but only for mowers that support message events after the WebSocket connection
is ready.
"""
coordinator = config_entry.runtime_data
entity_registry = er.async_get(hass)
restored_mowers = {
restored_mowers: set[str] = {
entry.unique_id.removesuffix("_message")
for entry in er.async_entries_for_config_entry(
entity_registry, config_entry.entry_id
@@ -49,14 +50,20 @@ async def async_setup_entry(
if entry.domain == EVENT_DOMAIN
}
async_add_entities(
AutomowerMessageEventEntity(mower_id, coordinator)
for mower_id in restored_mowers
if mower_id in coordinator.data
)
@callback
def _on_ws_ready() -> None:
async_add_entities(
AutomowerMessageEventEntity(mower_id, coordinator, websocket_alive=True)
for mower_id in restored_mowers
if mower_id in coordinator.data
)
coordinator.api.unregister_ws_ready_callback(_on_ws_ready)
coordinator.api.register_ws_ready_callback(_on_ws_ready)
@callback
def _handle_message(msg: SingleMessageData) -> None:
"""Add entity dynamically if a new mower sends messages."""
if msg.id in restored_mowers:
return
@@ -78,11 +85,17 @@ class AutomowerMessageEventEntity(AutomowerBaseEntity, EventEntity):
self,
mower_id: str,
coordinator: AutomowerDataUpdateCoordinator,
*,
websocket_alive: bool | None = None,
) -> None:
"""Initialize Automower message event entity."""
super().__init__(mower_id, coordinator)
self._attr_unique_id = f"{mower_id}_message"
self.websocket_alive: bool = coordinator.websocket_alive
self.websocket_alive: bool = (
websocket_alive
if websocket_alive is not None
else coordinator.websocket_alive
)
@property
def available(self) -> bool:

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2025.7.0"]
"requirements": ["pydrawise==2025.9.0"]
}

View File

@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["imeon_inverter_api==0.3.14"],
"requirements": ["imeon_inverter_api==0.4.0"],
"ssdp": [
{
"manufacturer": "IMEON",

View File

@@ -615,7 +615,7 @@ class IntentHandleView(http.HomeAssistantView):
intent_result = await intent.async_handle(
hass, DOMAIN, intent_name, slots, "", self.context(request)
)
except intent.IntentHandleError as err:
except (intent.IntentHandleError, intent.MatchFailedError) as err:
intent_result = intent.IntentResponse(language=language)
intent_result.async_set_speech(str(err))

View File

@@ -29,8 +29,7 @@ from .const import (
DEFAULT_LANGUAGE,
DOMAIN,
)
from .coordinator import JewishCalendarData, JewishCalendarUpdateCoordinator
from .entity import JewishCalendarConfigEntry
from .entity import JewishCalendarConfigEntry, JewishCalendarData
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
@@ -70,7 +69,7 @@ async def async_setup_entry(
)
)
data = JewishCalendarData(
config_entry.runtime_data = JewishCalendarData(
language,
diaspora,
location,
@@ -78,11 +77,8 @@ async def async_setup_entry(
havdalah_offset,
)
coordinator = JewishCalendarUpdateCoordinator(hass, config_entry, data)
await coordinator.async_config_entry_first_refresh()
config_entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
@@ -90,13 +86,7 @@ async def async_unload_entry(
hass: HomeAssistant, config_entry: JewishCalendarConfigEntry
) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
):
coordinator = config_entry.runtime_data
if coordinator.event_unsub:
coordinator.event_unsub()
return unload_ok
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
async def async_migrate_entry(

View File

@@ -72,7 +72,8 @@ class JewishCalendarBinarySensor(JewishCalendarEntity, BinarySensorEntity):
@property
def is_on(self) -> bool:
"""Return true if sensor is on."""
return self.entity_description.is_on(self.coordinator.zmanim)(dt_util.now())
zmanim = self.make_zmanim(dt.date.today())
return self.entity_description.is_on(zmanim)(dt_util.now())
def _update_times(self, zmanim: Zmanim) -> list[dt.datetime | None]:
"""Return a list of times to update the sensor."""

View File

@@ -1,116 +0,0 @@
"""Data update coordinator for Jewish calendar."""
from dataclasses import dataclass
import datetime as dt
import logging
from hdate import HDateInfo, Location, Zmanim
from hdate.translator import Language, set_language
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import event
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
import homeassistant.util.dt as dt_util
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarUpdateCoordinator]
@dataclass
class JewishCalendarData:
"""Jewish Calendar runtime dataclass."""
language: Language
diaspora: bool
location: Location
candle_lighting_offset: int
havdalah_offset: int
dateinfo: HDateInfo | None = None
zmanim: Zmanim | None = None
class JewishCalendarUpdateCoordinator(DataUpdateCoordinator[JewishCalendarData]):
"""Data update coordinator class for Jewish calendar."""
config_entry: JewishCalendarConfigEntry
event_unsub: CALLBACK_TYPE | None = None
def __init__(
self,
hass: HomeAssistant,
config_entry: JewishCalendarConfigEntry,
data: JewishCalendarData,
) -> None:
"""Initialize the coordinator."""
super().__init__(hass, _LOGGER, name=DOMAIN, config_entry=config_entry)
self.data = data
self._unsub_update: CALLBACK_TYPE | None = None
set_language(data.language)
async def _async_update_data(self) -> JewishCalendarData:
"""Return HDate and Zmanim for today."""
now = dt_util.now()
_LOGGER.debug("Now: %s Location: %r", now, self.data.location)
today = now.date()
self.data.dateinfo = HDateInfo(today, self.data.diaspora)
self.data.zmanim = self.make_zmanim(today)
self.async_schedule_future_update()
return self.data
@callback
def async_schedule_future_update(self) -> None:
"""Schedule the next update of the sensor for the upcoming midnight."""
# Cancel any existing update
if self._unsub_update:
self._unsub_update()
self._unsub_update = None
# Calculate the next midnight
next_midnight = dt_util.start_of_local_day() + dt.timedelta(days=1)
_LOGGER.debug("Scheduling next update at %s", next_midnight)
# Schedule update at next midnight
self._unsub_update = event.async_track_point_in_time(
self.hass, self._handle_midnight_update, next_midnight
)
@callback
def _handle_midnight_update(self, _now: dt.datetime) -> None:
"""Handle midnight update callback."""
self._unsub_update = None
self.async_set_updated_data(self.data)
async def async_shutdown(self) -> None:
"""Cancel any scheduled updates when the coordinator is shutting down."""
await super().async_shutdown()
if self._unsub_update:
self._unsub_update()
self._unsub_update = None
def make_zmanim(self, date: dt.date) -> Zmanim:
"""Create a Zmanim object."""
return Zmanim(
date=date,
location=self.data.location,
candle_lighting_offset=self.data.candle_lighting_offset,
havdalah_offset=self.data.havdalah_offset,
)
@property
def zmanim(self) -> Zmanim:
"""Return the current Zmanim."""
assert self.data.zmanim is not None, "Zmanim data not available"
return self.data.zmanim
@property
def dateinfo(self) -> HDateInfo:
"""Return the current HDateInfo."""
assert self.data.dateinfo is not None, "HDateInfo data not available"
return self.data.dateinfo

View File

@@ -24,5 +24,5 @@ async def async_get_config_entry_diagnostics(
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"data": async_redact_data(asdict(entry.runtime_data.data), TO_REDACT),
"data": async_redact_data(asdict(entry.runtime_data), TO_REDACT),
}

View File

@@ -1,22 +1,48 @@
"""Entity representing a Jewish Calendar sensor."""
from abc import abstractmethod
from dataclasses import dataclass
import datetime as dt
import logging
from hdate import Zmanim
from hdate import HDateInfo, Location, Zmanim
from hdate.translator import Language, set_language
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers import event
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .coordinator import JewishCalendarConfigEntry, JewishCalendarUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarData]
class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]):
@dataclass
class JewishCalendarDataResults:
"""Jewish Calendar results dataclass."""
dateinfo: HDateInfo
zmanim: Zmanim
@dataclass
class JewishCalendarData:
"""Jewish Calendar runtime dataclass."""
language: Language
diaspora: bool
location: Location
candle_lighting_offset: int
havdalah_offset: int
results: JewishCalendarDataResults | None = None
class JewishCalendarEntity(Entity):
"""An HA implementation for Jewish Calendar entity."""
_attr_has_entity_name = True
@@ -29,13 +55,23 @@ class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]):
description: EntityDescription,
) -> None:
"""Initialize a Jewish Calendar entity."""
super().__init__(config_entry.runtime_data)
self.entity_description = description
self._attr_unique_id = f"{config_entry.entry_id}-{description.key}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, config_entry.entry_id)},
)
self.data = config_entry.runtime_data
set_language(self.data.language)
def make_zmanim(self, date: dt.date) -> Zmanim:
"""Create a Zmanim object."""
return Zmanim(
date=date,
location=self.data.location,
candle_lighting_offset=self.data.candle_lighting_offset,
havdalah_offset=self.data.havdalah_offset,
)
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
@@ -56,9 +92,10 @@ class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]):
def _schedule_update(self) -> None:
"""Schedule the next update of the sensor."""
now = dt_util.now()
zmanim = self.make_zmanim(now.date())
update = dt_util.start_of_local_day() + dt.timedelta(days=1)
for update_time in self._update_times(self.coordinator.zmanim):
for update_time in self._update_times(zmanim):
if update_time is not None and now < update_time < update:
update = update_time
@@ -73,4 +110,17 @@ class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]):
"""Update the sensor data."""
self._update_unsub = None
self._schedule_update()
self.create_results(now)
self.async_write_ha_state()
def create_results(self, now: dt.datetime | None = None) -> None:
"""Create the results for the sensor."""
if now is None:
now = dt_util.now()
_LOGGER.debug("Now: %s Location: %r", now, self.data.location)
today = now.date()
zmanim = self.make_zmanim(today)
dateinfo = HDateInfo(today, diaspora=self.data.diaspora)
self.data.results = JewishCalendarDataResults(dateinfo, zmanim)

View File

@@ -19,7 +19,7 @@ from homeassistant.components.sensor import (
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
import homeassistant.util.dt as dt_util
from homeassistant.util import dt as dt_util
from .entity import JewishCalendarConfigEntry, JewishCalendarEntity
@@ -236,18 +236,25 @@ class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
return []
return [self.entity_description.next_update_fn(zmanim)]
def get_dateinfo(self) -> HDateInfo:
def get_dateinfo(self, now: dt.datetime | None = None) -> HDateInfo:
"""Get the next date info."""
now = dt_util.now()
if self.data.results is None:
self.create_results()
assert self.data.results is not None, "Results should be available"
if now is None:
now = dt_util.now()
today = now.date()
zmanim = self.make_zmanim(today)
update = None
if self.entity_description.next_update_fn:
update = self.entity_description.next_update_fn(self.coordinator.zmanim)
update = self.entity_description.next_update_fn(zmanim)
_LOGGER.debug("Today: %s, update: %s", now.date(), update)
_LOGGER.debug("Today: %s, update: %s", today, update)
if update is not None and now >= update:
return self.coordinator.dateinfo.next_day
return self.coordinator.dateinfo
return self.data.results.dateinfo.next_day
return self.data.results.dateinfo
class JewishCalendarSensor(JewishCalendarBaseSensor):
@@ -264,9 +271,7 @@ class JewishCalendarSensor(JewishCalendarBaseSensor):
super().__init__(config_entry, description)
# Set the options for enumeration sensors
if self.entity_description.options_fn is not None:
self._attr_options = self.entity_description.options_fn(
self.coordinator.data.diaspora
)
self._attr_options = self.entity_description.options_fn(self.data.diaspora)
@property
def native_value(self) -> str | int | dt.datetime | None:
@@ -290,8 +295,9 @@ class JewishCalendarTimeSensor(JewishCalendarBaseSensor):
@property
def native_value(self) -> dt.datetime | None:
"""Return the state of the sensor."""
if self.data.results is None:
self.create_results()
assert self.data.results is not None, "Results should be available"
if self.entity_description.value_fn is None:
return self.coordinator.zmanim.zmanim[self.entity_description.key].local
return self.entity_description.value_fn(
self.get_dateinfo(), self.coordinator.make_zmanim
)
return self.data.results.zmanim.zmanim[self.entity_description.key].local
return self.entity_description.value_fn(self.get_dateinfo(), self.make_zmanim)

View File

@@ -285,13 +285,19 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight
group_address_switch_green_state=conf.get_state_and_passive(
CONF_COLOR, CONF_GA_GREEN_SWITCH
),
group_address_brightness_green=conf.get_write(CONF_GA_GREEN_BRIGHTNESS),
group_address_brightness_green=conf.get_write(
CONF_COLOR, CONF_GA_GREEN_BRIGHTNESS
),
group_address_brightness_green_state=conf.get_state_and_passive(
CONF_COLOR, CONF_GA_GREEN_BRIGHTNESS
),
group_address_switch_blue=conf.get_write(CONF_GA_BLUE_SWITCH),
group_address_switch_blue_state=conf.get_state_and_passive(CONF_GA_BLUE_SWITCH),
group_address_brightness_blue=conf.get_write(CONF_GA_BLUE_BRIGHTNESS),
group_address_switch_blue=conf.get_write(CONF_COLOR, CONF_GA_BLUE_SWITCH),
group_address_switch_blue_state=conf.get_state_and_passive(
CONF_COLOR, CONF_GA_BLUE_SWITCH
),
group_address_brightness_blue=conf.get_write(
CONF_COLOR, CONF_GA_BLUE_BRIGHTNESS
),
group_address_brightness_blue_state=conf.get_state_and_passive(
CONF_COLOR, CONF_GA_BLUE_BRIGHTNESS
),

View File

@@ -13,11 +13,12 @@ from homeassistant.util.ulid import ulid_now
from ..const import DOMAIN
from .const import CONF_DATA
from .migration import migrate_1_to_2
from .migration import migrate_1_to_2, migrate_2_1_to_2_2
_LOGGER = logging.getLogger(__name__)
STORAGE_VERSION: Final = 2
STORAGE_VERSION_MINOR: Final = 2
STORAGE_KEY: Final = f"{DOMAIN}/config_store.json"
type KNXPlatformStoreModel = dict[str, dict[str, Any]] # unique_id: configuration
@@ -54,9 +55,13 @@ class _KNXConfigStoreStorage(Store[KNXConfigStoreModel]):
) -> dict[str, Any]:
"""Migrate to the new version."""
if old_major_version == 1:
# version 2 introduced in 2025.8
# version 2.1 introduced in 2025.8
migrate_1_to_2(old_data)
if old_major_version <= 2 and old_minor_version < 2:
# version 2.2 introduced in 2025.9.2
migrate_2_1_to_2_2(old_data)
return old_data
@@ -71,7 +76,9 @@ class KNXConfigStore:
"""Initialize config store."""
self.hass = hass
self.config_entry = config_entry
self._store = _KNXConfigStoreStorage(hass, STORAGE_VERSION, STORAGE_KEY)
self._store = _KNXConfigStoreStorage(
hass, STORAGE_VERSION, STORAGE_KEY, minor_version=STORAGE_VERSION_MINOR
)
self.data = KNXConfigStoreModel(entities={})
self._platform_controllers: dict[Platform, PlatformControllerBase] = {}

View File

@@ -118,27 +118,31 @@ COVER_KNX_SCHEMA = AllSerializeFirst(
vol.Schema(
{
"section_binary_control": KNXSectionFlat(),
vol.Optional(CONF_GA_UP_DOWN): GASelector(state=False),
vol.Optional(CONF_GA_UP_DOWN): GASelector(state=False, valid_dpt="1"),
vol.Optional(CoverConf.INVERT_UPDOWN): selector.BooleanSelector(),
"section_stop_control": KNXSectionFlat(),
vol.Optional(CONF_GA_STOP): GASelector(state=False),
vol.Optional(CONF_GA_STEP): GASelector(state=False),
vol.Optional(CONF_GA_STOP): GASelector(state=False, valid_dpt="1"),
vol.Optional(CONF_GA_STEP): GASelector(state=False, valid_dpt="1"),
"section_position_control": KNXSectionFlat(collapsible=True),
vol.Optional(CONF_GA_POSITION_SET): GASelector(state=False),
vol.Optional(CONF_GA_POSITION_STATE): GASelector(write=False),
vol.Optional(CONF_GA_POSITION_SET): GASelector(
state=False, valid_dpt="5.001"
),
vol.Optional(CONF_GA_POSITION_STATE): GASelector(
write=False, valid_dpt="5.001"
),
vol.Optional(CoverConf.INVERT_POSITION): selector.BooleanSelector(),
"section_tilt_control": KNXSectionFlat(collapsible=True),
vol.Optional(CONF_GA_ANGLE): GASelector(),
vol.Optional(CONF_GA_ANGLE): GASelector(valid_dpt="5.001"),
vol.Optional(CoverConf.INVERT_ANGLE): selector.BooleanSelector(),
"section_travel_time": KNXSectionFlat(),
vol.Optional(
vol.Required(
CoverConf.TRAVELLING_TIME_UP, default=25
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=1000, step=0.1, unit_of_measurement="s"
)
),
vol.Optional(
vol.Required(
CoverConf.TRAVELLING_TIME_DOWN, default=25
): selector.NumberSelector(
selector.NumberSelectorConfig(
@@ -240,19 +244,19 @@ LIGHT_KNX_SCHEMA = AllSerializeFirst(
write_required=True, valid_dpt="5.001"
),
"section_blue": KNXSectionFlat(),
vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector(
write_required=True, valid_dpt="5.001"
),
vol.Optional(CONF_GA_BLUE_SWITCH): GASelector(
write_required=False, valid_dpt="1"
),
"section_white": KNXSectionFlat(),
vol.Optional(CONF_GA_WHITE_BRIGHTNESS): GASelector(
vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector(
write_required=True, valid_dpt="5.001"
),
"section_white": KNXSectionFlat(),
vol.Optional(CONF_GA_WHITE_SWITCH): GASelector(
write_required=False, valid_dpt="1"
),
vol.Optional(CONF_GA_WHITE_BRIGHTNESS): GASelector(
write_required=True, valid_dpt="5.001"
),
},
),
GroupSelectOption(
@@ -310,7 +314,7 @@ LIGHT_KNX_SCHEMA = AllSerializeFirst(
SWITCH_KNX_SCHEMA = vol.Schema(
{
"section_switch": KNXSectionFlat(),
vol.Required(CONF_GA_SWITCH): GASelector(write_required=True),
vol.Required(CONF_GA_SWITCH): GASelector(write_required=True, valid_dpt="1"),
vol.Optional(CONF_INVERT, default=False): selector.BooleanSelector(),
vol.Optional(CONF_RESPOND_TO_READ, default=False): selector.BooleanSelector(),
vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(),

View File

@@ -4,6 +4,7 @@ from typing import Any
from homeassistant.const import Platform
from ..const import CONF_RESPOND_TO_READ
from . import const as store_const
@@ -40,3 +41,12 @@ def _migrate_light_schema_1_to_2(light_knx_data: dict[str, Any]) -> None:
if color:
light_knx_data[store_const.CONF_COLOR] = color
def migrate_2_1_to_2_2(data: dict[str, Any]) -> None:
"""Migrate from schema 2.1 to schema 2.2."""
if b_sensors := data.get("entities", {}).get(Platform.BINARY_SENSOR):
for b_sensor in b_sensors.values():
# "respond_to_read" was never used for binary_sensor and is not valid
# in the new schema. It was set as default in Store schema v1 and v2.1
b_sensor["knx"].pop(CONF_RESPOND_TO_READ, None)

View File

@@ -2,7 +2,9 @@
import asyncio
import logging
import uuid
from aiohttp import ClientSession
from packaging import version
from pylamarzocco import (
LaMarzoccoBluetoothClient,
@@ -11,6 +13,7 @@ from pylamarzocco import (
)
from pylamarzocco.const import FirmwareType
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from pylamarzocco.util import InstallationKey, generate_installation_key
from homeassistant.components.bluetooth import async_discovered_service_info
from homeassistant.const import (
@@ -19,13 +22,14 @@ from homeassistant.const import (
CONF_TOKEN,
CONF_USERNAME,
Platform,
__version__,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import CONF_USE_BLUETOOTH, DOMAIN
from .const import CONF_INSTALLATION_KEY, CONF_USE_BLUETOOTH, DOMAIN
from .coordinator import (
LaMarzoccoConfigEntry,
LaMarzoccoConfigUpdateCoordinator,
@@ -60,7 +64,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
cloud_client = LaMarzoccoCloudClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
client=async_create_clientsession(hass),
installation_key=InstallationKey.from_json(entry.data[CONF_INSTALLATION_KEY]),
client=create_client_session(hass),
)
try:
@@ -166,45 +171,50 @@ async def async_migrate_entry(
hass: HomeAssistant, entry: LaMarzoccoConfigEntry
) -> bool:
"""Migrate config entry."""
if entry.version > 3:
if entry.version > 4:
# guard against downgrade from a future version
return False
if entry.version == 1:
if entry.version in (1, 2):
_LOGGER.error(
"Migration from version 1 is no longer supported, please remove and re-add the integration"
"Migration from version 1 or 2 is no longer supported, please remove and re-add the integration"
)
return False
if entry.version == 2:
if entry.version == 3:
installation_key = generate_installation_key(str(uuid.uuid4()).lower())
cloud_client = LaMarzoccoCloudClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
installation_key=installation_key,
client=create_client_session(hass),
)
try:
things = await cloud_client.list_things()
await cloud_client.async_register_client()
except (AuthFail, RequestNotSuccessful) as exc:
_LOGGER.error("Migration failed with error %s", exc)
return False
v3_data = {
CONF_USERNAME: entry.data[CONF_USERNAME],
CONF_PASSWORD: entry.data[CONF_PASSWORD],
CONF_TOKEN: next(
(
thing.ble_auth_token
for thing in things
if thing.serial_number == entry.unique_id
),
None,
),
}
if CONF_MAC in entry.data:
v3_data[CONF_MAC] = entry.data[CONF_MAC]
hass.config_entries.async_update_entry(
entry,
data=v3_data,
version=3,
data={
**entry.data,
CONF_INSTALLATION_KEY: installation_key.to_json(),
},
version=4,
)
_LOGGER.debug("Migrated La Marzocco config entry to version 2")
_LOGGER.debug("Migrated La Marzocco config entry to version 4")
return True
def create_client_session(hass: HomeAssistant) -> ClientSession:
"""Create a ClientSession with La Marzocco specific headers."""
return async_create_clientsession(
hass,
headers={
"X-Client": "HOME_ASSISTANT",
"X-Client-Build": __version__,
},
)

View File

@@ -5,11 +5,13 @@ from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
import uuid
from aiohttp import ClientSession
from pylamarzocco import LaMarzoccoCloudClient
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from pylamarzocco.models import Thing
from pylamarzocco.util import InstallationKey, generate_installation_key
import voluptuous as vol
from homeassistant.components.bluetooth import (
@@ -33,7 +35,6 @@ from homeassistant.const import (
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
@@ -45,7 +46,8 @@ from homeassistant.helpers.selector import (
)
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import CONF_USE_BLUETOOTH, DOMAIN
from . import create_client_session
from .const import CONF_INSTALLATION_KEY, CONF_USE_BLUETOOTH, DOMAIN
from .coordinator import LaMarzoccoConfigEntry
CONF_MACHINE = "machine"
@@ -57,9 +59,10 @@ _LOGGER = logging.getLogger(__name__)
class LmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for La Marzocco."""
VERSION = 3
VERSION = 4
_client: ClientSession
_installation_key: InstallationKey
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -83,13 +86,18 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
**user_input,
}
self._client = async_create_clientsession(self.hass)
self._client = create_client_session(self.hass)
self._installation_key = generate_installation_key(
str(uuid.uuid4()).lower()
)
cloud_client = LaMarzoccoCloudClient(
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
client=self._client,
installation_key=self._installation_key,
)
try:
await cloud_client.async_register_client()
things = await cloud_client.list_things()
except AuthFail:
_LOGGER.debug("Server rejected login credentials")
@@ -184,6 +192,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
title=selected_device.name,
data={
**self._config,
CONF_INSTALLATION_KEY: self._installation_key.to_json(),
CONF_TOKEN: self._things[serial_number].ble_auth_token,
},
)

View File

@@ -5,3 +5,4 @@ from typing import Final
DOMAIN: Final = "lamarzocco"
CONF_USE_BLUETOOTH: Final = "use_bluetooth"
CONF_INSTALLATION_KEY: Final = "installation_key"

View File

@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.0.11"]
"requirements": ["pylamarzocco==2.1.0"]
}

View File

@@ -54,6 +54,6 @@
"requirements": [
"aiolifx==1.2.1",
"aiolifx-effects==0.3.2",
"aiolifx-themes==0.6.4"
"aiolifx-themes==1.0.2"
]
}

View File

@@ -57,7 +57,8 @@
},
"extra_fields": {
"brightness_pct": "Brightness",
"flash": "Flash"
"flash": "Flash",
"for": "[%key:common::device_automation::extra_fields::for%]"
}
},
"entity_component": {

View File

@@ -634,8 +634,8 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="NitrogenDioxideSensor",
translation_key="nitrogen_dioxide",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,

View File

@@ -435,6 +435,9 @@
"evse_soc": {
"name": "State of charge"
},
"nitrogen_dioxide": {
"name": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]"
},
"pump_control_mode": {
"name": "Control mode",
"state": {

View File

@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp[default]==2025.08.11"],
"requirements": ["yt-dlp[default]==2025.09.05"],
"single_config_entry": true
}

View File

@@ -2,12 +2,13 @@
from __future__ import annotations
import asyncio.timeouts
import asyncio
from collections.abc import Callable
from dataclasses import dataclass
from datetime import timedelta
import logging
from aiohttp import ClientResponseError
from pymiele import MieleAction, MieleAPI, MieleDevice
from homeassistant.config_entries import ConfigEntry
@@ -66,7 +67,22 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]):
self.devices = devices
actions = {}
for device_id in devices:
actions_json = await self.api.get_actions(device_id)
try:
actions_json = await self.api.get_actions(device_id)
except ClientResponseError as err:
_LOGGER.debug(
"Error fetching actions for device %s: Status: %s, Message: %s",
device_id,
err.status,
err.message,
)
actions_json = {}
except TimeoutError:
_LOGGER.debug(
"Timeout fetching actions for device %s",
device_id,
)
actions_json = {}
actions[device_id] = MieleAction(actions_json)
return MieleCoordinatorData(devices=devices, actions=actions)

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/mill",
"iot_class": "local_polling",
"loggers": ["mill", "mill_local"],
"requirements": ["millheater==0.12.5", "mill-local==0.3.0"]
"requirements": ["millheater==0.13.1", "mill-local==0.3.0"]
}

View File

@@ -267,8 +267,8 @@ CLIMATE_SCHEMA = vol.All(
{
vol.Required(CONF_TARGET_TEMP): hvac_fixedsize_reglist_validator,
vol.Optional(CONF_TARGET_TEMP_WRITE_REGISTERS, default=False): cv.boolean,
vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(float),
vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(int),
vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(int),
vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
vol.Exclusive(CONF_HVAC_ONOFF_COIL, "hvac_onoff_type"): cv.positive_int,

View File

@@ -62,7 +62,6 @@ from .const import (
CONF_VIRTUAL_COUNT,
CONF_WRITE_TYPE,
CONF_ZERO_SUPPRESS,
SIGNAL_START_ENTITY,
SIGNAL_STOP_ENTITY,
DataType,
)
@@ -95,18 +94,10 @@ class BasePlatform(Entity):
self._attr_name = entry[CONF_NAME]
self._attr_device_class = entry.get(CONF_DEVICE_CLASS)
def get_optional_numeric_config(config_name: str) -> int | float | None:
if (val := entry.get(config_name)) is None:
return None
assert isinstance(val, (float, int)), (
f"Expected float or int but {config_name} was {type(val)}"
)
return val
self._min_value = get_optional_numeric_config(CONF_MIN_VALUE)
self._max_value = get_optional_numeric_config(CONF_MAX_VALUE)
self._min_value = entry.get(CONF_MIN_VALUE)
self._max_value = entry.get(CONF_MAX_VALUE)
self._nan_value = entry.get(CONF_NAN_VALUE)
self._zero_suppress = get_optional_numeric_config(CONF_ZERO_SUPPRESS)
self._zero_suppress = entry.get(CONF_ZERO_SUPPRESS)
@abstractmethod
async def _async_update(self) -> None:
@@ -143,7 +134,6 @@ class BasePlatform(Entity):
self._cancel_call()
self._cancel_call = None
self._attr_available = False
self.async_write_ha_state()
async def async_await_connection(self, _now: Any) -> None:
"""Wait for first connect."""
@@ -162,11 +152,6 @@ class BasePlatform(Entity):
self.async_on_remove(
async_dispatcher_connect(self.hass, SIGNAL_STOP_ENTITY, self.async_disable)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_START_ENTITY, self.async_local_update
)
)
class BaseStructPlatform(BasePlatform, RestoreEntity):
@@ -352,7 +337,6 @@ class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity):
return
if self._verify_delay:
assert self._verify_delay == 1
if self._cancel_call:
self._cancel_call()
self._cancel_call = None

View File

@@ -64,7 +64,8 @@ class ModbusLight(BaseSwitch, LightEntity):
self._attr_color_mode = self._detect_color_mode(config)
self._attr_supported_color_modes = {self._attr_color_mode}
# Set min/max kelvin values if the mode is COLOR_TEMP
self._attr_min_color_temp_kelvin: int = LIGHT_DEFAULT_MIN_KELVIN
self._attr_max_color_temp_kelvin: int = LIGHT_DEFAULT_MAX_KELVIN
if self._attr_color_mode == ColorMode.COLOR_TEMP:
self._attr_min_color_temp_kelvin = config.get(
CONF_MIN_TEMP, LIGHT_DEFAULT_MIN_KELVIN
@@ -193,9 +194,6 @@ class ModbusLight(BaseSwitch, LightEntity):
def _convert_modbus_percent_to_temperature(self, percent: int) -> int:
"""Convert Modbus scale (0-100) to the color temperature in Kelvin (2000-7000 К)."""
assert isinstance(self._attr_min_color_temp_kelvin, int) and isinstance(
self._attr_max_color_temp_kelvin, int
)
return round(
self._attr_min_color_temp_kelvin
+ (
@@ -216,9 +214,6 @@ class ModbusLight(BaseSwitch, LightEntity):
def _convert_color_temp_to_modbus(self, kelvin: int) -> int:
"""Convert color temperature from Kelvin to the Modbus scale (0-100)."""
assert isinstance(self._attr_min_color_temp_kelvin, int) and isinstance(
self._attr_max_color_temp_kelvin, int
)
return round(
LIGHT_MODBUS_SCALE_MIN
+ (kelvin - self._attr_min_color_temp_kelvin)

View File

@@ -223,6 +223,8 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
# Brightness is supported and no supported_color_modes are set,
# so set brightness as the supported color mode.
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
else:
self._attr_supported_color_modes = {ColorMode.ONOFF}
def _update_color(self, values: dict[str, Any]) -> None:
color_mode: str = values["color_mode"]

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_push",
"loggers": ["aionfty"],
"quality_scale": "bronze",
"requirements": ["aiontfy==0.5.4"]
"requirements": ["aiontfy==0.5.5"]
}

View File

@@ -10,7 +10,7 @@ import logging
from ohme import ApiException, OhmeApiClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
@@ -83,6 +83,21 @@ class OhmeAdvancedSettingsCoordinator(OhmeBaseCoordinator):
coordinator_name = "Advanced Settings"
def __init__(
self, hass: HomeAssistant, config_entry: OhmeConfigEntry, client: OhmeApiClient
) -> None:
"""Initialise coordinator."""
super().__init__(hass, config_entry, client)
@callback
def _dummy_listener() -> None:
pass
# This coordinator is used by the API library to determine whether the
# charger is online and available. It is therefore required even if no
# entities are using it.
self.async_add_listener(_dummy_listener)
async def _internal_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self.client.async_get_advanced_settings()

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["ohme==1.5.1"]
"requirements": ["ohme==1.5.2"]
}

View File

@@ -181,7 +181,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator]
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_BEARING)
@property
def visibility(self) -> float | str | None:
def native_visibility(self) -> float | None:
"""Return visibility."""
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_VISIBILITY_DISTANCE)

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.15.4"]
"requirements": ["opower==0.15.5"]
}

View File

@@ -14,6 +14,9 @@
"changed_states": "[%key:common::device_automation::trigger_type::changed_states%]",
"turned_on": "[%key:common::device_automation::trigger_type::turned_on%]",
"turned_off": "[%key:common::device_automation::trigger_type::turned_off%]"
},
"extra_fields": {
"for": "[%key:common::device_automation::extra_fields::for%]"
}
},
"entity_component": {

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/schlage",
"iot_class": "cloud_polling",
"requirements": ["pyschlage==2025.7.3"]
"requirements": ["pyschlage==2025.9.0"]
}

View File

@@ -365,7 +365,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
unit converter supports both the native and the suggested units of measurement.
"""
# Make sure we can convert the units
if (
if self.native_unit_of_measurement != suggested_unit_of_measurement and (
(unit_converter := UNIT_CONVERTERS.get(self.device_class)) is None
or self.__native_unit_of_measurement_compat
not in unit_converter.VALID_UNITS

View File

@@ -3,6 +3,7 @@
import asyncio
from contextlib import suppress
import aiohttp
from sharkiq import (
AylaApi,
SharkIqAuthError,
@@ -15,7 +16,7 @@ from homeassistant import exceptions
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import (
API_TIMEOUT,
@@ -56,10 +57,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
data={**config_entry.data, CONF_REGION: SHARKIQ_REGION_DEFAULT},
)
new_websession = async_create_clientsession(
hass,
cookie_jar=aiohttp.CookieJar(unsafe=True, quote_cookie=False),
)
ayla_api = get_ayla_api(
username=config_entry.data[CONF_USERNAME],
password=config_entry.data[CONF_PASSWORD],
websession=async_get_clientsession(hass),
websession=new_websession,
europe=(config_entry.data[CONF_REGION] == SHARKIQ_REGION_EUROPE),
)
@@ -94,7 +100,7 @@ async def async_disconnect_or_timeout(coordinator: SharkIqUpdateCoordinator):
await coordinator.ayla_api.async_sign_out()
async def async_update_options(hass, config_entry):
async def async_update_options(hass: HomeAssistant, config_entry):
"""Update options."""
await hass.config_entries.async_reload(config_entry.entry_id)

View File

@@ -15,7 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import selector
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import (
DOMAIN,
@@ -44,15 +44,19 @@ async def _validate_input(
hass: HomeAssistant, data: Mapping[str, Any]
) -> dict[str, str]:
"""Validate the user input allows us to connect."""
new_websession = async_create_clientsession(
hass,
cookie_jar=aiohttp.CookieJar(unsafe=True, quote_cookie=False),
)
ayla_api = get_ayla_api(
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
websession=async_get_clientsession(hass),
websession=new_websession,
europe=(data[CONF_REGION] == SHARKIQ_REGION_EUROPE),
)
try:
async with asyncio.timeout(10):
async with asyncio.timeout(15):
LOGGER.debug("Initialize connection to Ayla networks API")
await ayla_api.async_sign_in()
except (TimeoutError, aiohttp.ClientError, TypeError) as error:

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/sharkiq",
"iot_class": "cloud_polling",
"loggers": ["sharkiq"],
"requirements": ["sharkiq==1.1.1"]
"requirements": ["sharkiq==1.4.0"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pymodbus", "pysmarty2"],
"requirements": ["pysmarty2==0.10.2"]
"requirements": ["pysmarty2==0.10.3"]
}

View File

@@ -410,7 +410,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
@soco_error()
def set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
self.soco.volume = int(volume * 100)
self.soco.volume = int(round(volume * 100))
@soco_error(UPNP_ERRORS_TO_IGNORE)
def set_shuffle(self, shuffle: bool) -> None:

View File

@@ -61,8 +61,15 @@ async def async_setup_entry(
if (
state := getattr(speaker.soco, select_data.soco_attribute, None)
) is not None:
setattr(speaker, select_data.speaker_attribute, state)
features.append(select_data)
try:
setattr(speaker, select_data.speaker_attribute, int(state))
features.append(select_data)
except ValueError:
_LOGGER.error(
"Invalid value for %s %s",
select_data.speaker_attribute,
state,
)
return features
async def _async_create_entities(speaker: SonosSpeaker) -> None:

View File

@@ -599,7 +599,12 @@ class SonosSpeaker:
for enum_var in (ATTR_DIALOG_LEVEL,):
if enum_var in variables:
setattr(self, f"{enum_var}_enum", variables[enum_var])
try:
setattr(self, f"{enum_var}_enum", int(variables[enum_var]))
except ValueError:
_LOGGER.error(
"Invalid value for %s %s", enum_var, variables[enum_var]
)
self.async_write_entity_states()

View File

@@ -116,6 +116,7 @@ CONTENT_TYPE_TO_CHILD_TYPE: dict[
MediaType.APPS: MediaType.APP,
MediaType.APP: MediaType.TRACK,
"favorite": None,
"track": MediaType.TRACK,
}

View File

@@ -607,7 +607,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
_media_content_type_list = (
query.media_content_type.lower().replace(", ", ",").split(",")
if query.media_content_type
else ["albums", "tracks", "artists", "genres"]
else ["albums", "tracks", "artists", "genres", "playlists"]
)
if query.media_content_type and set(_media_content_type_list).difference(

View File

@@ -14,6 +14,9 @@
"changed_states": "[%key:common::device_automation::trigger_type::changed_states%]",
"turned_on": "[%key:common::device_automation::trigger_type::turned_on%]",
"turned_off": "[%key:common::device_automation::trigger_type::turned_off%]"
},
"extra_fields": {
"for": "[%key:common::device_automation::extra_fields::for%]"
}
},
"entity_component": {

View File

@@ -291,6 +291,7 @@ class TractiveClient:
for switch, key in SWITCH_KEY_MAP.items():
if switch_data := event.get(key):
payload[switch] = switch_data["active"]
payload[ATTR_POWER_SAVING] = event.get("tracker_state_reason") == "POWER_SAVING"
self._dispatch_tracker_event(
TRACKER_SWITCH_STATUS_UPDATED, event["tracker_id"], payload
)

View File

@@ -18,6 +18,7 @@ from .const import (
ATTR_BUZZER,
ATTR_LED,
ATTR_LIVE_TRACKING,
ATTR_POWER_SAVING,
TRACKER_SWITCH_STATUS_UPDATED,
)
from .entity import TractiveEntity
@@ -104,7 +105,7 @@ class TractiveSwitch(TractiveEntity, SwitchEntity):
# We received an event, so the service is online and the switch entities should
# be available.
self._attr_available = True
self._attr_available = not event[ATTR_POWER_SAVING]
self._attr_is_on = event[self.entity_description.key]
self.async_write_ha_state()

View File

@@ -42,6 +42,6 @@
"documentation": "https://www.home-assistant.io/integrations/tuya",
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["tuya_iot"],
"loggers": ["tuya_sharing"],
"requirements": ["tuya-device-sharing-sdk==0.2.1"]
}

View File

@@ -5,6 +5,9 @@
"changed_states": "{entity_name} update availability changed",
"turned_on": "{entity_name} got an update available",
"turned_off": "{entity_name} became up-to-date"
},
"extra_fields": {
"for": "[%key:common::device_automation::extra_fields::for%]"
}
},
"entity_component": {

View File

@@ -59,5 +59,6 @@ class VeluxRainSensor(VeluxEntity, BinarySensorEntity):
LOGGER.error("Error fetching limitation data for cover %s", self.name)
return
# Velux windows with rain sensors report an opening limitation of 93 when rain is detected.
self._attr_is_on = limitation.min_value == 93
# Velux windows with rain sensors report an opening limitation of 93 or 100 (Velux GPU) when rain is detected.
# So far, only 93 and 100 have been observed in practice, documentation on this is non-existent AFAIK.
self._attr_is_on = limitation.min_value in {93, 100}

View File

@@ -4,8 +4,15 @@ from __future__ import annotations
from typing import Any, cast
from pyvlx import OpeningDevice, Position
from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter
from pyvlx import (
Awning,
Blind,
GarageDoor,
Gate,
OpeningDevice,
Position,
RollerShutter,
)
from homeassistant.components.cover import (
ATTR_POSITION,
@@ -97,7 +104,10 @@ class VeluxCover(VeluxEntity, CoverEntity):
@property
def is_closed(self) -> bool:
"""Return if the cover is closed."""
return self.node.position.closed
# do not use the node's closed state but rely on cover position
# until https://github.com/Julius2342/pyvlx/pull/543 is merged.
# once merged this can again return self.node.position.closed
return self.current_cover_position == 0
@property
def is_opening(self) -> bool:

View File

@@ -1,7 +1,7 @@
{
"domain": "velux",
"name": "Velux",
"codeowners": ["@Julius2342", "@DeerMaximum", "@pawlizio"],
"codeowners": ["@Julius2342", "@DeerMaximum", "@pawlizio", "@wollew"],
"config_flow": true,
"dhcp": [
{

View File

@@ -8,11 +8,14 @@ from typing import Any, cast
from aiohttp import ClientSession
from aiovodafone import VodafoneStationDevice, VodafoneStationSercommApi, exceptions
from homeassistant.components.device_tracker import DEFAULT_CONSIDER_HOME
from homeassistant.components.device_tracker import (
DEFAULT_CONSIDER_HOME,
DOMAIN as DEVICE_TRACKER_DOMAIN,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
@@ -71,16 +74,14 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
update_interval=timedelta(seconds=SCAN_INTERVAL),
config_entry=config_entry,
)
device_reg = dr.async_get(self.hass)
device_list = dr.async_entries_for_config_entry(
device_reg, self.config_entry.entry_id
)
entity_reg = er.async_get(hass)
self.previous_devices = {
connection[1].upper()
for device in device_list
for connection in device.connections
if connection[0] == dr.CONNECTION_NETWORK_MAC
entry.unique_id
for entry in er.async_entries_for_config_entry(
entity_reg, config_entry.entry_id
)
if entry.domain == DEVICE_TRACKER_DOMAIN
}
def _calculate_update_time_and_consider_home(

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiovodafone"],
"quality_scale": "platinum",
"requirements": ["aiovodafone==0.10.0"]
"requirements": ["aiovodafone==1.2.1"]
}

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["waterfurnace"],
"quality_scale": "legacy",
"requirements": ["waterfurnace==1.1.0"]
"requirements": ["waterfurnace==1.2.0"]
}

View File

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

View File

@@ -146,6 +146,8 @@ async def async_send_message( # noqa: C901
self.enable_starttls = use_tls
self.enable_direct_tls = use_tls
self.enable_plaintext = not use_tls
self["feature_mechanisms"].unencrypted_scram = not use_tls
self.use_ipv6 = False
self.add_event_handler("failed_all_auth", self.disconnect_on_login_fail)
self.add_event_handler("session_start", self.start)

View File

@@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 9
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)

View File

@@ -1898,11 +1898,25 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) -
@callback
def cleanup_restored_states_filter(event_data: Mapping[str, Any]) -> bool:
"""Clean up restored states filter."""
return bool(event_data["action"] == "remove")
return (event_data["action"] == "remove") or (
event_data["action"] == "update"
and "old_entity_id" in event_data
and event_data["entity_id"] != event_data["old_entity_id"]
)
@callback
def cleanup_restored_states(event: Event[EventEntityRegistryUpdatedData]) -> None:
"""Clean up restored states."""
if event.data["action"] == "update":
old_entity_id = event.data["old_entity_id"]
old_state = hass.states.get(old_entity_id)
if old_state is None or not old_state.attributes.get(ATTR_RESTORED):
return
hass.states.async_remove(old_entity_id, context=event.context)
if entry := registry.async_get(event.data["entity_id"]):
entry.write_unavailable_state(hass)
return
state = hass.states.get(event.data["entity_id"])
if state is None or not state.attributes.get(ATTR_RESTORED):

Some files were not shown because too many files have changed in this diff Show More