Compare commits

...

94 Commits

Author SHA1 Message Date
Franck Nijhof
808273962d Bump version to 2025.8.0b3 2025-08-05 12:01:54 +00:00
Martin Hjelmare
094fe43557 Fix Z-Wave duplicate provisioned device (#150008) 2025-08-05 11:56:00 +00:00
Michael Hansen
8f5bd51eef Bump wyoming to 1.7.2 (#150007) 2025-08-05 11:55:59 +00:00
Thomas55555
faf0ded854 Bump aioautomower to 2.1.2 (#150003) 2025-08-05 11:55:58 +00:00
Matthias Alphart
d20302f97b Update knx-frontend to 2025.8.4.154919 (#149991) 2025-08-05 11:55:57 +00:00
Grzegorz M
74c25496bc Bump icalendar from 6.1.0 to 6.3.1 for CalDav (#149990) 2025-08-05 11:55:56 +00:00
Petro31
67ecea0778 Create battery_level deprecation repair for template vacuum platform (#149987)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-08-05 11:55:55 +00:00
Robert Resch
164e5871cb Bump deebot-client to 13.6.0 (#149983) 2025-08-05 11:55:53 +00:00
Joakim Sørensen
7a9966120e Bump hass-nabucasa from 0.110.1 to 0.111.0 (#149977) 2025-08-05 11:55:52 +00:00
Martin Hjelmare
d810b4ca38 Bump zwave-js-server-python to 0.67.1 (#149972) 2025-08-05 11:55:51 +00:00
epenet
896062d669 Fix Tuya fan speeds with numeric values (#149971) 2025-08-05 11:54:02 +00:00
epenet
03bd133577 Rename Tuya fixture files (#149927) 2025-08-05 11:47:15 +00:00
Martin Hjelmare
4596c1644b Direct migrations with Z-Wave JS UI to docs (#149966) 2025-08-05 11:43:12 +00:00
Petro31
778fe96eb6 Fix optimistic covers (#149962) 2025-08-05 11:43:11 +00:00
Joost Lekkerkerker
a06557ed54 Pass config entry to Remote Calendar coordinator (#149958) 2025-08-05 11:43:10 +00:00
Joakim Sørensen
641621d184 Bump hass-nabucasa from 0.110.0 to 0.110.1 (#149956) 2025-08-05 11:43:09 +00:00
Joost Lekkerkerker
b163f2b855 Pass config entry to SMS coordinator (#149955) 2025-08-05 11:43:08 +00:00
Joost Lekkerkerker
0c0604e5bd Pass config entry to Fronius coordinator (#149954) 2025-08-05 11:43:07 +00:00
Joost Lekkerkerker
e0e4fc8afb Pass config entry to AsusWRT coordinator (#149953) 2025-08-05 11:43:05 +00:00
Joost Lekkerkerker
f832a2844f Pass config entry to Unifi coordinator (#149952) 2025-08-05 11:43:04 +00:00
Erik Montnemery
4b0b268227 Fix DeviceEntry.suggested_area deprecation warning (#149951) 2025-08-05 11:43:03 +00:00
Joost Lekkerkerker
dfc16d9f15 Pass config entry to Broadlink coordinator (#149949) 2025-08-05 11:43:02 +00:00
Joost Lekkerkerker
4e3309bd22 Pass config entry to Snoo coordinator (#149947) 2025-08-05 11:43:01 +00:00
Joost Lekkerkerker
a5a45ce59f Pass config entry to Smarttub coordinator (#149946) 2025-08-05 11:43:00 +00:00
Joost Lekkerkerker
6cb48da2f3 Pass config entry to Meteo France coordinator (#149945) 2025-08-05 11:42:59 +00:00
Joost Lekkerkerker
ab5aac47b2 Pass config entry to Kraken coordinator (#149944) 2025-08-05 11:42:58 +00:00
Joost Lekkerkerker
d50b9405f0 Pass config entry to Simplisafe coordinator (#149943) 2025-08-05 11:42:57 +00:00
Joost Lekkerkerker
a2722f08c4 Pass config entry to Mill coordinator (#149942) 2025-08-05 11:42:56 +00:00
Joost Lekkerkerker
aa700c3982 Pass config entry to hue coordinator (#149941) 2025-08-05 11:42:55 +00:00
Ståle Storø Hauknes
3b1bb41129 Airthings ContextVar warning (#149930) 2025-08-05 11:42:54 +00:00
Brett Adams
79ef51fb07 Fix credit sensor when there are no vehicles in Teslemetry (#149925) 2025-08-05 11:34:16 +00:00
andreimoraru
53769da55e Bump yt-dlp to 2025.07.21 (#149916)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-08-05 11:34:15 +00:00
Thomas55555
82d153a240 Fix options for error sensor in Husqvarna Automower (#149901) 2025-08-05 11:34:13 +00:00
Andrew Jackson
0dac635478 Bump aiomealie to 0.10.1 (#149890) 2025-08-05 11:34:12 +00:00
Tom
90fc7d314b Bump python-airos to 0.2.4 (#149885) 2025-08-05 11:34:10 +00:00
Mike Degatano
636c1b7e4f Add translation strings for unsupported OS version (#149837) 2025-08-05 11:34:09 +00:00
Christopher Fenner
49c23de2d2 Update sensor icons in Volvo integration (#149811) 2025-08-05 11:34:08 +00:00
Ludovic BOUÉ
e48820b2c1 Matter pump setpoint CurrentLevel limit (#149689) 2025-08-05 11:34:06 +00:00
Bram Kragten
2b7a434677 Bump version to 2025.8.0b2 2025-08-04 10:37:10 +02:00
puddly
9ef7c6c99a Bump ZHA to 0.0.65 (#149922) 2025-08-04 10:37:07 +02:00
J. Nick Koston
b789c11217 Bump dbus-fast to 2.44.3 (#149921) 2025-08-04 10:37:06 +02:00
J. Nick Koston
5e8cd19cc3 Bump aiodiscover to 2.7.1 (#149920) 2025-08-04 10:37:05 +02:00
J. Nick Koston
027052440d Bump yalexs-ble to 3.1.2 (#149917) 2025-08-04 10:37:04 +02:00
Maciej Bieniek
47a7ed4084 Bump imgw_pib to version 1.5.2 (#149892) 2025-08-04 10:37:03 +02:00
Martin Hjelmare
89f6cfeb81 Fix Z-Wave handling of driver ready event (#149879) 2025-08-04 10:37:02 +02:00
Joost Lekkerkerker
c268e57ba7 Bump python-open-router to 0.3.1 (#149873) 2025-08-04 10:37:01 +02:00
Andrea Turri
138c19126b Fix Miele hob translation keys (#149865) 2025-08-04 10:37:01 +02:00
Oliver
c459ceba73 Update denonavr to 1.1.2 (#149842) 2025-08-04 10:37:00 +02:00
Martin Hjelmare
8d0ceff652 Fix Z-Wave config entry state conditions in listen task (#149841) 2025-08-04 10:36:59 +02:00
peteS-UK
1d383e80a4 Fix initialisation of Apps and Radios list for Squeezebox (#149834) 2025-08-04 10:36:58 +02:00
Norbert Rittel
6a17a12be5 Update reference for volatile_organic_compounds_parts in template (#149831) 2025-08-04 10:36:57 +02:00
Norbert Rittel
3a8d962d34 Add translation for absolute_humidity device class to mqtt (#149818) 2025-08-04 10:36:56 +02:00
Norbert Rittel
7e5cf17cf4 Add translation for absolute_humidity device class to random (#149815) 2025-08-04 10:36:55 +02:00
Norbert Rittel
214940d04f Add translation for absolute_humidity device class to template (#149814) 2025-08-04 10:36:54 +02:00
Thomas D
6877fdaf5b Add scopes in config flow auth request for Volvo integration (#149813) 2025-08-04 10:36:53 +02:00
Norbert Rittel
35d0c254a2 Fix descriptions for template number fields (#149804) 2025-08-04 10:36:53 +02:00
epenet
9649fbc189 Fix tuya light supported color modes (#149793)
Co-authored-by: Erik <erik@montnemery.com>
2025-08-04 10:36:52 +02:00
Jamin
b60b1fc0c6 Bump VoIP utils to 0.3.4 (#149786) 2025-08-04 10:36:51 +02:00
Manu
6b93f6d75c Hide configuration URL when Uptime Kuma is installed locally (#149781) 2025-08-04 10:36:50 +02:00
starkillerOG
c8069a383e Bump motionblinds to 0.6.30 (#149764) 2025-08-04 10:36:49 +02:00
Nathan Spencer
6857e87b30 Bump pylitterbot to 2024.2.3 (#149763) 2025-08-04 10:36:48 +02:00
J. Nick Koston
a095631f4f Bump aioesphomeapi to 37.2.2 (#149755) 2025-08-04 10:36:48 +02:00
Copilot
c59fbdeec1 Fix ZHA ContextVar deprecation by passing config_entry (#149748)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joostlek <7083755+joostlek@users.noreply.github.com>
Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
Co-authored-by: TheJulianJES <6409465+TheJulianJES@users.noreply.github.com>
2025-08-04 10:36:47 +02:00
Erik Montnemery
b521b1e64c Make device suggested_area only influence new devices (#149758)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-08-04 10:34:42 +02:00
Erik Montnemery
073589ae19 Deprecate DeviceEntry.suggested_area (#149730) 2025-08-04 10:34:38 +02:00
Erik Montnemery
9435b0ad3a Fix flaky velbus test (#149743) 2025-08-04 10:34:15 +02:00
karwosts
1662d36125 Fix add_suggested_values_to_schema when the schema has sections (#149718)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-08-04 10:14:30 +02:00
Erik Montnemery
70e54fdadd Improve test of FlowHandler.add_suggested_values_to_schema (#149759) 2025-08-04 10:13:44 +02:00
Tom
38d0ebb8ba Add diagnostics to UISP AirOS (#149631) 2025-08-04 10:08:19 +02:00
Bram Kragten
15cb48badb Bump version to 2025.8.0b1 2025-07-31 19:01:26 +02:00
Erik Montnemery
22214e8d31 Fix kitchen_sink option flow (#149760) 2025-07-31 19:01:02 +02:00
Bram Kragten
fc04e0b2cc Update frontend to 20250731.0 (#149757) 2025-07-31 19:01:01 +02:00
Petro31
3fc6ebdb43 Fix unique_id in config validation for legacy weather platform (#149742) 2025-07-31 19:01:00 +02:00
Petro31
3ccb7deb3c Nitpick default translations for template integration (#149740) 2025-07-31 19:00:59 +02:00
Erik Montnemery
f5f63b914a Make _EventDeviceRegistryUpdatedData_Remove JSON serializable (#149734) 2025-07-31 19:00:58 +02:00
J. Nick Koston
bd0a3f5a5d Bump aioesphomeapi to 37.2.0 (#149732) 2025-07-31 19:00:58 +02:00
J. Nick Koston
ab9eebd092 Bump aioesphomeapi to 37.1.6 (#149715) 2025-07-31 19:00:57 +02:00
J. Nick Koston
68c43099d9 Fix ESPHome unnecessary probing on DHCP discovery (#149713) 2025-07-31 19:00:56 +02:00
Åke Strandberg
041c417164 Fix bug when interpreting miele action response (#149710) 2025-07-31 19:00:55 +02:00
Andrea Turri
537d09c697 Fix Miele induction hob empty state (#149706) 2025-07-31 19:00:54 +02:00
Roman Sivriver
21e3b8da92 Fix typo in backup log message (#149705) 2025-07-31 19:00:53 +02:00
Jan Bouwhuis
d390681360 Fix inconsistent use of the term 'target' and a typo in MQTT translation strings (#149703) 2025-07-31 19:00:53 +02:00
Åke Strandberg
918ec78348 Add missing translations for miele dishwasher (#149702) 2025-07-31 19:00:52 +02:00
starkillerOG
1deae3ee1a Bump reolink-aio to 0.14.5 (#149700) 2025-07-31 19:00:51 +02:00
Petro31
59eace67df Add translations for all fields in template integration (#149692)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-07-31 19:00:50 +02:00
Åke Strandberg
7eb7c66e3f Explicitly pass config_entry to miele coordinator (#149691) 2025-07-31 19:00:49 +02:00
Copilot
aa2941592d Fix ContextVar deprecation warning in homeassistant_hardware integration (#149687)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joostlek <7083755+joostlek@users.noreply.github.com>
Co-authored-by: mib1185 <35783820+mib1185@users.noreply.github.com>
2025-07-31 19:00:48 +02:00
Manu
29daf136d2 Fix KeyError in friends coordinator (#149684) 2025-07-31 19:00:47 +02:00
puddly
3da3cf7f52 Bump ZHA to 0.0.64 (#149683)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
Co-authored-by: abmantis <amfcalt@gmail.com>
2025-07-31 19:00:47 +02:00
Michael Hansen
d8c93d54d5 Bump intents to 2025.7.30 (#149678) 2025-07-31 19:00:46 +02:00
Jan Bouwhuis
0799ee9fba Fix translation string reference for MQTT climate subentry option (#149673) 2025-07-31 19:00:45 +02:00
Bram Kragten
9d31403984 2025.8.0b0 (#149675) 2025-07-30 17:13:15 +02:00
Bram Kragten
02f87cba9b Merge branch 'dev' into dev-rc 2025-07-30 17:07:48 +02:00
Bram Kragten
5b54784378 Bump version to 2025.8.0b0 2025-07-30 16:56:55 +02:00
329 changed files with 5949 additions and 2648 deletions

View File

@@ -6,11 +6,11 @@ import logging
from typing import Any
from airos.exceptions import (
ConnectionAuthenticationError,
ConnectionSetupError,
DataMissingError,
DeviceConnectionError,
KeyDataMissingError,
AirOSConnectionAuthenticationError,
AirOSConnectionSetupError,
AirOSDataMissingError,
AirOSDeviceConnectionError,
AirOSKeyDataMissingError,
)
import voluptuous as vol
@@ -59,13 +59,13 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
airos_data = await airos_device.status()
except (
ConnectionSetupError,
DeviceConnectionError,
AirOSConnectionSetupError,
AirOSDeviceConnectionError,
):
errors["base"] = "cannot_connect"
except (ConnectionAuthenticationError, DataMissingError):
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
errors["base"] = "invalid_auth"
except KeyDataMissingError:
except AirOSKeyDataMissingError:
errors["base"] = "key_data_missing"
except Exception:
_LOGGER.exception("Unexpected exception")

View File

@@ -6,10 +6,10 @@ import logging
from airos.airos8 import AirOS, AirOSData
from airos.exceptions import (
ConnectionAuthenticationError,
ConnectionSetupError,
DataMissingError,
DeviceConnectionError,
AirOSConnectionAuthenticationError,
AirOSConnectionSetupError,
AirOSDataMissingError,
AirOSDeviceConnectionError,
)
from homeassistant.config_entries import ConfigEntry
@@ -47,18 +47,22 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
try:
await self.airos_device.login()
return await self.airos_device.status()
except (ConnectionAuthenticationError,) as err:
except (AirOSConnectionAuthenticationError,) as err:
_LOGGER.exception("Error authenticating with airOS device")
raise ConfigEntryError(
translation_domain=DOMAIN, translation_key="invalid_auth"
) from err
except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err:
except (
AirOSConnectionSetupError,
AirOSDeviceConnectionError,
TimeoutError,
) as err:
_LOGGER.error("Error connecting to airOS device: %s", err)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
except (DataMissingError,) as err:
except (AirOSDataMissingError,) as err:
_LOGGER.error("Expected data not returned by airOS device: %s", err)
raise UpdateFailed(
translation_domain=DOMAIN,

View File

@@ -0,0 +1,33 @@
"""Diagnostics support for airOS."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from .coordinator import AirOSConfigEntry
IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related
HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address
TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD]
TO_REDACT_AIROS = [
"hostname", # Prevent leaking device naming
"essid", # Network SSID
"lat", # GPS latitude to prevent exposing location data.
"lon", # GPS longitude to prevent exposing location data.
*HW_REDACT,
*IP_REDACT,
]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AirOSConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"entry_data": async_redact_data(entry.data, TO_REDACT_HA),
"data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS),
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airos",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["airos==0.2.1"]
"requirements": ["airos==0.2.4"]
}

View File

@@ -41,7 +41,7 @@ rules:
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info: todo
discovery: todo
docs-data-update: done

View File

@@ -69,13 +69,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
translation_key="wireless_essid",
value_fn=lambda data: data.wireless.essid,
),
AirOSSensorEntityDescription(
key="wireless_mode",
translation_key="wireless_mode",
device_class=SensorDeviceClass.ENUM,
value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(),
options=WIRELESS_MODE_OPTIONS,
),
AirOSSensorEntityDescription(
key="wireless_antenna_gain",
translation_key="wireless_antenna_gain",

View File

@@ -43,13 +43,6 @@
"wireless_essid": {
"name": "Wireless SSID"
},
"wireless_mode": {
"name": "Wireless mode",
"state": {
"ap_ptp": "Access point",
"sta_ptp": "Station"
}
},
"wireless_antenna_gain": {
"name": "Antenna gain"
},

View File

@@ -7,21 +7,18 @@ import logging
from airthings import Airthings
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_SECRET
from .coordinator import AirthingsDataUpdateCoordinator
from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=6)
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
"""Set up Airthings from a config entry."""
@@ -31,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
async_get_clientsession(hass),
)
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry)
await coordinator.async_config_entry_first_refresh()

View File

@@ -5,6 +5,7 @@ import logging
from airthings import Airthings, AirthingsDevice, AirthingsError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -13,15 +14,23 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=6)
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
"""Coordinator for Airthings data updates."""
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
def __init__(
self,
hass: HomeAssistant,
airthings: Airthings,
config_entry: AirthingsConfigEntry,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_method=self._update_method,
update_interval=SCAN_INTERVAL,

View File

@@ -430,7 +430,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
"model": device.model,
"sw_version": device.sw_version,
"hw_version": device.hw_version,
"has_suggested_area": device.suggested_area is not None,
"has_configuration_url": device.configuration_url is not None,
"via_device": None,
}

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable, Mapping
from datetime import datetime, timedelta
import logging
from typing import Any
from typing import TYPE_CHECKING, Any
from pyasuswrt import AsusWrtError
@@ -40,6 +40,9 @@ from .const import (
SENSORS_CONNECTED_DEVICE,
)
if TYPE_CHECKING:
from . import AsusWrtConfigEntry
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
SCAN_INTERVAL = timedelta(seconds=30)
@@ -52,10 +55,13 @@ _LOGGER = logging.getLogger(__name__)
class AsusWrtSensorDataHandler:
"""Data handler for AsusWrt sensor."""
def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None:
def __init__(
self, hass: HomeAssistant, api: AsusWrtBridge, entry: AsusWrtConfigEntry
) -> None:
"""Initialize a AsusWrt sensor data handler."""
self._hass = hass
self._api = api
self._entry = entry
self._connected_devices = 0
async def _get_connected_devices(self) -> dict[str, int]:
@@ -91,6 +97,7 @@ class AsusWrtSensorDataHandler:
update_method=method,
# Polling interval. Will only be polled if there are subscribers.
update_interval=SCAN_INTERVAL if should_poll else None,
config_entry=self._entry,
)
await coordinator.async_refresh()
@@ -321,7 +328,9 @@ class AsusWrtRouter:
if self._sensors_data_handler:
return
self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api)
self._sensors_data_handler = AsusWrtSensorDataHandler(
self.hass, self._api, self._entry
)
self._sensors_data_handler.update_device_count(self._connected_devices)
sensors_types = await self._api.async_get_available_sensors()

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"]
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"]
}

View File

@@ -1119,7 +1119,7 @@ class BackupManager:
)
if unavailable_agents:
LOGGER.warning(
"Backup agents %s are not available, will backupp to %s",
"Backup agents %s are not available, will backup to %s",
unavailable_agents,
available_agents,
)

View File

@@ -20,7 +20,7 @@
"bluetooth-adapters==2.0.0",
"bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.2",
"dbus-fast==2.44.2",
"dbus-fast==2.44.3",
"habluetooth==4.0.1"
]
}

View File

@@ -64,6 +64,7 @@ class BroadlinkUpdateManager(ABC, Generic[_ApiT]):
device.hass,
_LOGGER,
name=f"{device.name} ({device.api.model} at {device.api.host[0]})",
config_entry=device.config,
update_method=self.async_update,
update_interval=self.SCAN_INTERVAL,
)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/caldav",
"iot_class": "cloud_polling",
"loggers": ["caldav", "vobject"],
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
"requirements": ["caldav==1.6.0", "icalendar==6.3.1"]
}

View File

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

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.23"]
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.7.30"]
}

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/denonavr",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.1.1"],
"requirements": ["denonavr==1.1.2"],
"ssdp": [
{
"manufacturer": "Denon",

View File

@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.0",
"aiodiscover==2.7.0",
"aiodiscover==2.7.1",
"cached-ipaddress==0.10.0"
]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==13.6.0"]
}

View File

@@ -116,6 +116,8 @@ async def async_get_config_entry_diagnostics(
entities.append({"entity": entity_dict, "state": state_dict})
device_dict = asdict(device)
device_dict.pop("_cache", None)
# This can be removed when suggested_area is removed from DeviceEntry
device_dict.pop("_suggested_area")
device_entities.append({"device": device_dict, "entities": entities})
# remove envoy serial

View File

@@ -316,10 +316,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
# Don't call _fetch_device_info() for ignored entries
raise AbortFlow("already_configured")
configured_host: str | None = entry.data.get(CONF_HOST)
configured_port: int | None = entry.data.get(CONF_PORT)
if configured_host == host and configured_port == port:
configured_port: int = entry.data.get(CONF_PORT, DEFAULT_PORT)
# When port is None (from DHCP discovery), only compare hosts
if configured_host == host and (port is None or configured_port == port):
# Don't probe to verify the mac is correct since
# the host and port matches.
# the host matches (and port matches if provided).
raise AbortFlow("already_configured")
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
await self._fetch_device_info(host, port or configured_port, configured_psk)

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==37.1.5",
"aioesphomeapi==37.2.2",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.1.0"
],

View File

@@ -106,6 +106,7 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_logger_{self.host}",
config_entry=self.config_entry,
)
await self.logger_coordinator.async_config_entry_first_refresh()
@@ -120,6 +121,7 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_meters_{self.host}",
config_entry=self.config_entry,
)
)
@@ -129,6 +131,7 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_ohmpilot_{self.host}",
config_entry=self.config_entry,
)
)
@@ -138,6 +141,7 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_power_flow_{self.host}",
config_entry=self.config_entry,
)
)
@@ -147,6 +151,7 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_storages_{self.host}",
config_entry=self.config_entry,
)
)
@@ -206,6 +211,7 @@ class FroniusSolarNet:
logger=_LOGGER,
name=_inverter_name,
inverter_info=_inverter_info,
config_entry=self.config_entry,
)
if self.config_entry.state == ConfigEntryState.LOADED:
await _coordinator.async_refresh()

View File

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

View File

@@ -225,6 +225,10 @@
"unsupported_virtualization_image": {
"title": "Unsupported system - Incorrect OS image for virtualization",
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this."
},
"unsupported_os_version": {
"title": "Unsupported system - Home Assistant OS version",
"description": "System is unsupported because the Home Assistant OS version in use is not supported. Use the link to learn more and how to fix this."
}
},
"entity": {

View File

@@ -12,6 +12,7 @@ from ha_silabs_firmware_client import (
ManifestMissing,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -24,13 +25,20 @@ FIRMWARE_REFRESH_INTERVAL = timedelta(hours=8)
class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]):
"""Coordinator to manage firmware updates."""
def __init__(self, hass: HomeAssistant, session: ClientSession, url: str) -> None:
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
session: ClientSession,
url: str,
) -> None:
"""Initialize the firmware update coordinator."""
super().__init__(
hass,
_LOGGER,
name="firmware update coordinator",
update_interval=FIRMWARE_REFRESH_INTERVAL,
config_entry=config_entry,
)
self.hass = hass
self.session = session

View File

@@ -124,6 +124,7 @@ def _async_create_update_entity(
config_entry=config_entry,
update_coordinator=FirmwareUpdateCoordinator(
hass,
config_entry,
session,
NABU_CASA_FIRMWARE_RELEASES_URL,
),

View File

@@ -129,6 +129,7 @@ def _async_create_update_entity(
config_entry=config_entry,
update_coordinator=FirmwareUpdateCoordinator(
hass,
config_entry,
session,
NABU_CASA_FIRMWARE_RELEASES_URL,
),

View File

@@ -163,6 +163,7 @@ async def async_setup_entry(
name="light",
update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update),
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
request_refresh_debouncer=Debouncer(
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
),
@@ -197,6 +198,7 @@ async def async_setup_entry(
name="group",
update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update),
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
request_refresh_debouncer=Debouncer(
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
),

View File

@@ -53,6 +53,7 @@ class SensorManager:
LOGGER,
name="sensor",
update_method=self.async_update_data,
config_entry=bridge.config_entry,
update_interval=self.SCAN_INTERVAL,
request_refresh_debouncer=debounce.Debouncer(
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==2.1.1"]
"requirements": ["aioautomower==2.1.2"]
}

View File

@@ -71,10 +71,10 @@ ERROR_KEYS = [
"cutting_drive_motor_2_defect",
"cutting_drive_motor_3_defect",
"cutting_height_blocked",
"cutting_height_problem",
"cutting_height_problem_curr",
"cutting_height_problem_dir",
"cutting_height_problem_drive",
"cutting_height_problem",
"cutting_motor_problem",
"cutting_stopped_slope_too_steep",
"cutting_system_blocked",
@@ -117,7 +117,6 @@ ERROR_KEYS = [
"no_accurate_position_from_satellites",
"no_confirmed_position",
"no_drive",
"no_error",
"no_loop_signal",
"no_power_in_charging_station",
"no_response_from_charger",
@@ -169,8 +168,8 @@ ERROR_KEYS = [
]
ERROR_KEY_LIST = list(
dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES])
ERROR_KEY_LIST = sorted(
set(ERROR_KEYS) | {state.lower() for state in ERROR_STATES} | {"no_error"}
)
INACTIVE_REASONS: list = [

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["imgw_pib==1.5.1"]
"requirements": ["imgw_pib==1.5.2"]
}

View File

@@ -99,7 +99,7 @@ class OptionsFlowHandler(OptionsFlowWithReload):
),
}
)
self.add_suggested_values_to_schema(
data_schema = self.add_suggested_values_to_schema(
data_schema,
{"section_1": {"int": self.config_entry.options.get(CONF_INT, 10)}},
)

View File

@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.8.0",
"xknxproject==3.8.2",
"knx-frontend==2025.7.23.50952"
"knx-frontend==2025.8.4.154919"
],
"single_config_entry": true
}

View File

@@ -135,6 +135,7 @@ class KrakenData:
self._hass,
_LOGGER,
name=DOMAIN,
config_entry=self._config_entry,
update_method=self.async_update,
update_interval=timedelta(
seconds=self._config_entry.options[CONF_SCAN_INTERVAL]

View File

@@ -13,5 +13,5 @@
"iot_class": "cloud_push",
"loggers": ["pylitterbot"],
"quality_scale": "bronze",
"requirements": ["pylitterbot==2024.2.2"]
"requirements": ["pylitterbot==2024.2.3"]
}

View File

@@ -285,7 +285,9 @@ DISCOVERY_SCHEMAS = [
native_min_value=0.5,
native_step=0.5,
device_to_ha=(
lambda x: None if x is None else x / 2 # Matter range (1-200)
lambda x: None
if x is None
else min(x, 200) / 2 # Matter range (1-200, capped at 200)
),
ha_to_device=lambda x: round(x * 2), # HA range 0.5100.0%
mode=NumberMode.SLIDER,

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["aiomealie==0.10.0"]
"requirements": ["aiomealie==0.10.1"]
}

View File

@@ -174,7 +174,8 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
if list_item.display.strip() != stripped_item_summary:
update_shopping_item.note = stripped_item_summary
update_shopping_item.position = position
update_shopping_item.is_food = False
if update_shopping_item.is_food is not None:
update_shopping_item.is_food = False
update_shopping_item.food_id = None
update_shopping_item.quantity = 0.0
update_shopping_item.checked = item.status == TodoItemStatus.COMPLETED
@@ -249,7 +250,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
mutate_shopping_item.note = item.note
mutate_shopping_item.checked = item.checked
if item.is_food:
if item.is_food or item.food_id:
mutate_shopping_item.food_id = item.food_id
mutate_shopping_item.unit_id = item.unit_id

View File

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

View File

@@ -63,6 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass,
_LOGGER,
name=f"Météo-France forecast for city {entry.title}",
config_entry=entry,
update_method=_async_update_data_forecast_forecast,
update_interval=SCAN_INTERVAL,
)
@@ -80,6 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass,
_LOGGER,
name=f"Météo-France rain for city {entry.title}",
config_entry=entry,
update_method=_async_update_data_rain,
update_interval=SCAN_INTERVAL_RAIN,
)
@@ -103,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass,
_LOGGER,
name=f"Météo-France alert for department {department}",
config_entry=entry,
update_method=_async_update_data_alert,
update_interval=SCAN_INTERVAL,
)

View File

@@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> boo
) from err
# Setup MieleAPI and coordinator for data fetch
coordinator = MieleDataUpdateCoordinator(hass, auth)
coordinator = MieleDataUpdateCoordinator(hass, entry, auth)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator

View File

@@ -431,6 +431,16 @@ DISHWASHER_PROGRAM_ID: dict[int, str] = {
38: "quick_power_wash",
42: "tall_items",
44: "power_wash",
200: "eco",
202: "automatic",
203: "comfort_wash",
204: "power_wash",
205: "intensive",
207: "extra_quiet",
209: "comfort_wash_plus",
210: "gentle",
214: "maintenance",
215: "rinse_salt",
}
TUMBLE_DRYER_PROGRAM_ID: dict[int, str] = {
-1: "no_program", # Extrapolated from other device types.

View File

@@ -42,12 +42,14 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]):
def __init__(
self,
hass: HomeAssistant,
config_entry: MieleConfigEntry,
api: AsyncConfigEntryAuth,
) -> None:
"""Initialize the Miele data coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=timedelta(seconds=120),
)

View File

@@ -731,7 +731,7 @@ class MielePlateSensor(MieleSensor):
)
).name
if self.device.state_plate_step
else PlatePowerStep.plate_step_0
else PlatePowerStep.plate_step_0.name
)

View File

@@ -203,7 +203,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse:
else {}
),
}
if item["parameters"]
if item.get("parameters")
else {}
),
}

View File

@@ -203,27 +203,27 @@
"plate": {
"name": "Plate {plate_no}",
"state": {
"power_step_0": "0",
"power_step_warm": "Warming",
"power_step_1": "1",
"power_step_2": "1\u2022",
"power_step_3": "2",
"power_step_4": "2\u2022",
"power_step_5": "3",
"power_step_6": "3\u2022",
"power_step_7": "4",
"power_step_8": "4\u2022",
"power_step_9": "5",
"power_step_10": "5\u2022",
"power_step_11": "6",
"power_step_12": "6\u2022",
"power_step_13": "7",
"power_step_14": "7\u2022",
"power_step_15": "8",
"power_step_16": "8\u2022",
"power_step_17": "9",
"power_step_18": "9\u2022",
"power_step_boost": "Boost"
"plate_step_0": "0",
"plate_step_warm": "Warming",
"plate_step_1": "1",
"plate_step_2": "1\u2022",
"plate_step_3": "2",
"plate_step_4": "2\u2022",
"plate_step_5": "3",
"plate_step_6": "3\u2022",
"plate_step_7": "4",
"plate_step_8": "4\u2022",
"plate_step_9": "5",
"plate_step_10": "5\u2022",
"plate_step_11": "6",
"plate_step_12": "6\u2022",
"plate_step_13": "7",
"plate_step_14": "7\u2022",
"plate_step_15": "8",
"plate_step_16": "8\u2022",
"plate_step_17": "9",
"plate_step_18": "9\u2022",
"plate_step_boost": "Boost"
}
},
"drying_step": {
@@ -485,6 +485,8 @@
"cook_bacon": "Cook bacon",
"biscuits_short_crust_pastry_1_tray": "Biscuits, short crust pastry (1 tray)",
"biscuits_short_crust_pastry_2_trays": "Biscuits, short crust pastry (2 trays)",
"comfort_wash": "Comfort wash",
"comfort_wash_plus": "Comfort wash plus",
"cool_air": "Cool air",
"corn_on_the_cob": "Corn on the cob",
"cottons": "Cottons",
@@ -827,6 +829,7 @@
"rice_pudding_steam_cooking": "Rice pudding (steam cooking)",
"rinse": "Rinse",
"rinse_out_lint": "Rinse out lint",
"rinse_salt": "Rinse salt",
"risotto": "Risotto",
"ristretto": "Ristretto",
"roast_beef_low_temperature_cooking": "Roast beef (low temperature cooking)",

View File

@@ -43,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
historic_data_coordinator = MillHistoricDataUpdateCoordinator(
hass,
entry,
mill_data_connection=mill_data_connection,
)
historic_data_coordinator.async_add_listener(lambda: None)

View File

@@ -60,6 +60,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator):
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
*,
mill_data_connection: Mill,
) -> None:
@@ -70,6 +71,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator):
hass,
_LOGGER,
name="MillHistoricDataUpdateCoordinator",
config_entry=config_entry,
)
async def _async_update_data(self):

View File

@@ -21,5 +21,5 @@
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"iot_class": "local_push",
"loggers": ["motionblinds"],
"requirements": ["motionblinds==0.6.29"]
"requirements": ["motionblinds==0.6.30"]
}

View File

@@ -426,7 +426,7 @@
},
"data_description": {
"payload_off": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]",
"payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]",
"payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_on%]",
"power_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the power command topic. The `value` parameter is the payload set for payload \"on\" or payload \"off\".",
"power_command_topic": "The MQTT topic to publish commands to change the climate power state. Sends the payload configured with payload \"on\" or payload \"off\". [Learn more.]({url}#power_command_topic)"
}
@@ -802,17 +802,17 @@
"data": {
"max_humidity": "Maximum humidity",
"min_humidity": "Minimum humidity",
"target_humidity_command_template": "Humidity command template",
"target_humidity_command_topic": "Humidity command topic",
"target_humidity_state_template": "Humidity state template",
"target_humidity_state_topic": "Humidity state topic"
"target_humidity_command_template": "Target humidity command template",
"target_humidity_command_topic": "Target humidity command topic",
"target_humidity_state_template": "Target humidity state template",
"target_humidity_state_topic": "Target humidity state topic"
},
"data_description": {
"max_humidity": "The maximum target humidity that can be set.",
"min_humidity": "The minimum target humidity that can be set.",
"target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the humidity command topic.",
"target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the target humidity command topic.",
"target_humidity_command_topic": "The MQTT topic to publish commands to change the climate target humidity. [Learn more.]({url}#humidity_command_topic)",
"target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the humidity state topic with.",
"target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the target humidity state topic with.",
"target_humidity_state_topic": "The MQTT topic to subscribe for changes of the target humidity. [Learn more.]({url}#humidity_state_topic)"
}
},
@@ -838,7 +838,7 @@
"temperature_low_state_topic": "Lower temperature state topic"
},
"data_description": {
"initial": "The climate initalizes with this target temperature.",
"initial": "The climate initializes with this target temperature.",
"max_temp": "The maximum target temperature that can be set.",
"min_temp": "The minimum target temperature that can be set.",
"precision": "The precision in degrees the thermostat is working at.",
@@ -1104,6 +1104,7 @@
},
"device_class_sensor": {
"options": {
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
"area": "[%key:component::sensor::entity_component::area::name%]",
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",

View File

@@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["openai==1.93.3", "python-open-router==0.3.0"]
"requirements": ["openai==1.93.3", "python-open-router==0.3.1"]
}

View File

@@ -6,7 +6,7 @@ from abc import abstractmethod
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any
from typing import TYPE_CHECKING, Any
from psnawp_api.core.psnawp_exceptions import (
PSNAWPAuthenticationError,
@@ -29,7 +29,7 @@ from homeassistant.exceptions import (
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_ACCOUNT_ID, DOMAIN
from .const import DOMAIN
from .helpers import PlaystationNetwork, PlaystationNetworkData
_LOGGER = logging.getLogger(__name__)
@@ -176,7 +176,9 @@ class PlaystationNetworkFriendDataCoordinator(
def _setup(self) -> None:
"""Set up the coordinator."""
self.user = self.psn.psn.user(account_id=self.subentry.data[CONF_ACCOUNT_ID])
if TYPE_CHECKING:
assert self.subentry.unique_id
self.user = self.psn.psn.user(account_id=self.subentry.unique_id)
self.profile = self.user.profile()
async def _async_setup(self) -> None:

View File

@@ -82,6 +82,7 @@
},
"sensor_device_class": {
"options": {
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
"area": "[%key:component::sensor::entity_component::area::name%]",

View File

@@ -39,6 +39,7 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]):
_LOGGER,
name=f"{DOMAIN}_{config_entry.title}",
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
always_update=True,
)
self._client = get_async_client(hass)

View File

@@ -19,5 +19,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.14.4"]
"requirements": ["reolink-aio==0.14.5"]
}

View File

@@ -573,6 +573,7 @@ class SimpliSafe:
self._hass,
LOGGER,
name=self.entry.title,
config_entry=self.entry,
update_interval=DEFAULT_SCAN_INTERVAL,
update_method=self.async_update,
)

View File

@@ -74,6 +74,7 @@ class SmartTubController:
self._hass,
_LOGGER,
name=DOMAIN,
config_entry=entry,
update_method=self.async_update_data,
update_interval=timedelta(seconds=SCAN_INTERVAL),
)

View File

@@ -83,8 +83,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not gateway:
raise ConfigEntryNotReady(f"Cannot find device {device}")
signal_coordinator = SignalCoordinator(hass, gateway)
network_coordinator = NetworkCoordinator(hass, gateway)
signal_coordinator = SignalCoordinator(hass, entry, gateway)
network_coordinator = NetworkCoordinator(hass, entry, gateway)
# Fetch initial data so we have data when entities subscribe
await signal_coordinator.async_config_entry_first_refresh()

View File

@@ -16,13 +16,14 @@ _LOGGER = logging.getLogger(__name__)
class SignalCoordinator(DataUpdateCoordinator):
"""Signal strength coordinator."""
def __init__(self, hass, gateway):
def __init__(self, hass, entry, gateway):
"""Initialize signal strength coordinator."""
super().__init__(
hass,
_LOGGER,
name="Device signal state",
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
config_entry=entry,
)
self._gateway = gateway
@@ -38,13 +39,14 @@ class SignalCoordinator(DataUpdateCoordinator):
class NetworkCoordinator(DataUpdateCoordinator):
"""Network info coordinator."""
def __init__(self, hass, gateway):
def __init__(self, hass, entry, gateway):
"""Initialize network info coordinator."""
super().__init__(
hass,
_LOGGER,
name="Device network state",
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
config_entry=entry,
)
self._gateway = gateway

View File

@@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SnooConfigEntry) -> bool
coordinators: dict[str, SnooCoordinator] = {}
tasks = []
for device in devices:
coordinators[device.serialNumber] = SnooCoordinator(hass, device, snoo)
coordinators[device.serialNumber] = SnooCoordinator(hass, entry, device, snoo)
tasks.append(coordinators[device.serialNumber].setup())
await asyncio.gather(*tasks)
entry.runtime_data = coordinators

View File

@@ -19,11 +19,18 @@ class SnooCoordinator(DataUpdateCoordinator[SnooData]):
config_entry: SnooConfigEntry
def __init__(self, hass: HomeAssistant, device: SnooDevice, snoo: Snoo) -> None:
def __init__(
self,
hass: HomeAssistant,
entry: SnooConfigEntry,
device: SnooDevice,
snoo: Snoo,
) -> None:
"""Set up Snoo Coordinator."""
super().__init__(
hass,
name=device.name,
config_entry=entry,
logger=_LOGGER,
)
self.device_unique_id = device.serialNumber

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import contextlib
from dataclasses import dataclass, field
import logging
from typing import Any
from pysqueezebox import Player
@@ -21,6 +22,8 @@ from homeassistant.helpers.network import is_internal_request
from .const import DOMAIN, UNPLAYABLE_TYPES
_LOGGER = logging.getLogger(__name__)
LIBRARY = [
"favorites",
"artists",
@@ -138,18 +141,42 @@ class BrowseData:
self.squeezebox_id_by_type.update(SQUEEZEBOX_ID_BY_TYPE)
self.media_type_to_squeezebox.update(MEDIA_TYPE_TO_SQUEEZEBOX)
def add_new_command(self, cmd: str | MediaType, type: str) -> None:
"""Add items to maps for new apps or radios."""
self.known_apps_radios.add(cmd)
self.media_type_to_squeezebox[cmd] = cmd
self.squeezebox_id_by_type[cmd] = type
self.content_type_media_class[cmd] = {
"item": MediaClass.DIRECTORY,
"children": MediaClass.TRACK,
}
self.content_type_to_child_type[cmd] = MediaType.TRACK
def _add_new_command_to_browse_data(
browse_data: BrowseData, cmd: str | MediaType, type: str
) -> None:
"""Add items to maps for new apps or radios."""
browse_data.media_type_to_squeezebox[cmd] = cmd
browse_data.squeezebox_id_by_type[cmd] = type
browse_data.content_type_media_class[cmd] = {
"item": MediaClass.DIRECTORY,
"children": MediaClass.TRACK,
}
browse_data.content_type_to_child_type[cmd] = MediaType.TRACK
async def async_init(self, player: Player, browse_limit: int) -> None:
"""Initialize known apps and radios from the player."""
cmd = ["apps", 0, browse_limit]
result = await player.async_query(*cmd)
for app in result["appss_loop"]:
app_cmd = "app-" + app["cmd"]
if app_cmd not in self.known_apps_radios:
self.add_new_command(app_cmd, "item_id")
_LOGGER.debug(
"Adding new command %s to browse data for player %s",
app_cmd,
player.player_id,
)
cmd = ["radios", 0, browse_limit]
result = await player.async_query(*cmd)
for app in result["radioss_loop"]:
app_cmd = "app-" + app["cmd"]
if app_cmd not in self.known_apps_radios:
self.add_new_command(app_cmd, "item_id")
_LOGGER.debug(
"Adding new command %s to browse data for player %s",
app_cmd,
player.player_id,
)
def _build_response_apps_radios_category(
@@ -292,8 +319,7 @@ async def build_item_response(
app_cmd = "app-" + item["cmd"]
if app_cmd not in browse_data.known_apps_radios:
browse_data.known_apps_radios.add(app_cmd)
_add_new_command_to_browse_data(browse_data, app_cmd, "item_id")
browse_data.add_new_command(app_cmd, "item_id")
child_media = _build_response_apps_radios_category(
browse_data=browse_data, cmd=app_cmd, item=item

View File

@@ -311,6 +311,11 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
)
return None
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
await self._browse_data.async_init(self._player, self.browse_limit)
async def async_will_remove_from_hass(self) -> None:
"""Remove from list of known players when removed from hass."""
self.coordinator.config_entry.runtime_data.known_player_ids.remove(

View File

@@ -216,6 +216,7 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity):
_entity_id_format = ENTITY_ID_FORMAT
_optimistic_entity = True
_extra_optimistic_options = (CONF_POSITION,)
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.

View File

@@ -20,6 +20,7 @@ class AbstractTemplateEntity(Entity):
_entity_id_format: str
_optimistic_entity: bool = False
_extra_optimistic_options: tuple[str, ...] | None = None
_template: Template | None = None
def __init__(
@@ -35,9 +36,14 @@ class AbstractTemplateEntity(Entity):
if self._optimistic_entity:
self._template = config.get(CONF_STATE)
self._attr_assumed_state = self._template is None or config.get(
CONF_OPTIMISTIC, False
)
optimistic = self._template is None
if self._extra_optimistic_options:
optimistic = optimistic and all(
config.get(option) is None
for option in self._extra_optimistic_options
)
self._attr_assumed_state = optimistic or config.get(CONF_OPTIMISTIC, False)
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
self.entity_id = async_generate_entity_id(

View File

@@ -2,6 +2,7 @@
"common": {
"advanced_options": "Advanced options",
"availability": "Availability template",
"availability_description": "Defines a template to get the `available` state of the entity. If the template either fails to render or returns `True`, `\"1\"`, `\"true\"`, `\"yes\"`, `\"on\"`, `\"enable\"`, or a non-zero number, the entity will be `available`. If the template returns any other value, the entity will be `unavailable`. If not configured, the entity will always be `available`. Note that the string comparison is not case sensitive; `\"TrUe\"` and `\"yEs\"` are allowed.",
"code_format": "Code format",
"device_class": "Device class",
"device_id_description": "Select a device to link to this entity.",
@@ -28,13 +29,26 @@
"code_format": "[%key:component::template::common::code_format%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"value_template": "Defines a template to set the state of the alarm panel. Valid output values from the template are `armed_away`, `armed_home`, `armed_night`, `armed_vacation`, `arming`, `disarmed`, `pending`, and `triggered`.",
"disarm": "Defines actions to run when the alarm control panel is disarmed. Receives variable `code`.",
"arm_away": "Defines actions to run when the alarm control panel is armed to `arm_away`. Receives variable `code`.",
"arm_custom_bypass": "Defines actions to run when the alarm control panel is armed to `arm_custom_bypass`. Receives variable `code`.",
"arm_home": "Defines actions to run when the alarm control panel is armed to `arm_home`. Receives variable `code`.",
"arm_night": "Defines actions to run when the alarm control panel is armed to `arm_night`. Receives variable `code`.",
"arm_vacation": "Defines actions to run when the alarm control panel is armed to `arm_vacation`. Receives variable `code`.",
"trigger": "Defines actions to run when the alarm control panel is triggered. Receives variable `code`.",
"code_arm_required": "If true, the code is required to arm the alarm.",
"code_format": "One of `number`, `text` or `no_code`. Format for the code used to arm/disarm the alarm."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -48,13 +62,17 @@
"state": "[%key:component::template::common::state%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "The sensor is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -68,13 +86,17 @@
"press": "Actions on press"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"press": "Defines actions to run when button is pressed."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -99,13 +121,16 @@
"close_cover": "Defines actions to run when the cover is closed.",
"stop_cover": "Defines actions to run when the cover is stopped.",
"position": "Defines a template to get the position of the cover. Value values are numbers between `0` (`closed`) and `100` (`open`).",
"set_cover_position": "Defines actions to run when the cover is given a `set_cover_position` command."
"set_cover_position": "Defines actions to run when the cover is given a `set_cover_position` command. Receives variable `position`."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -124,11 +149,11 @@
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "Defines a template to get the state of the fan. Valid values: `on`, `off`.",
"state": "The fan is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`.",
"turn_off": "Defines actions to run when the fan is turned off.",
"turn_on": "Defines actions to run when the fan is turned on.",
"turn_on": "Defines actions to run when the fan is turned on. Receives variables `percentage` and/or `preset_mode`.",
"percentage": "Defines a template to get the speed percentage of the fan.",
"set_percentage": "Defines actions to run when the fan is given a speed percentage command.",
"set_percentage": "Defines actions to run when the fan is given a speed percentage command. Receives variable `percentage`.",
"speed_count": "The number of speeds the fan supports. Used to calculate the percentage step for the `fan.increase_speed` and `fan.decrease_speed` actions."
},
"sections": {
@@ -136,6 +161,9 @@
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -149,13 +177,18 @@
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"url": "Defines a template to get the URL on which the image is served.",
"verify_ssl": "Enable or disable SSL certificate verification. Disable to use an http URL, or if you have a self-signed SSL certificate and havent installed the CA certificate to enable verification."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -176,13 +209,25 @@
"set_temperature": "Actions on set color temperature"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "The light is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`.",
"turn_off": "Defines actions to run when the light is turned off.",
"turn_on": "Defines actions to run when the light is turned on.",
"level": "Defines a template to get the brightness of the light. Valid values are 0 to 255.",
"set_level": "Defines actions to run when the light is given a brightness command. The script will only be called if the `turn_on` call only has `brightness`, and optionally `transition`. Receives variables `brightness` and, optionally, `transition`.",
"hs": "Defines a template to get the HS color of the light. Must render a tuple (hue, saturation).",
"set_hs": "Defines actions to run when the light is given a hs color command. Available variables: `hs` as a tuple, `h` and `s`.",
"temperature": "Defines a template to get the color temperature of the light.",
"set_temperature": "Defines actions to run when the light is given a color temperature command. Receives variable `color_temp_kelvin`. May also receive variables `brightness` and/or `transition`."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -199,13 +244,21 @@
"open": "Actions on open"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "Defines a template to set the state of the lock. The lock is locked if the template evaluates to `True`, `true`, `on`, or `locked`. The lock is unlocked if the template evaluates to `False`, `false`, `off`, or `unlocked`. Other valid states are `jammed`, `opening`, `locking`, `open`, and `unlocking`.",
"lock": "Defines actions to run when the lock is locked.",
"unlock": "Defines actions to run when the lock is unlocked.",
"code_format": "Defines a template to get the `code_format` attribute of the lock.",
"open": "Defines actions to run when the lock is opened."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -223,13 +276,22 @@
"unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "Template for the number's current value.",
"step": "Defines the number's increment/decrement step.",
"set_value": "Defines actions to run when the number is set to a value. Receives variable `value`.",
"max": "Defines the number's maximum value.",
"min": "Defines the number's minimum value.",
"unit_of_measurement": "Defines the unit of measurement of the number, if any."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -244,13 +306,19 @@
"options": "Available options"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "Template for the selects current value.",
"select_option": "Defines actions to run when an `option` from the `options` list is selected. Receives variable `option`.",
"options": "Template for the selects available options."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -266,13 +334,18 @@
"unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "Defines a template to get the state of the sensor. If the sensor is numeric, i.e. it has a `state_class` or a `unit_of_measurement`, the state template must render to a number or to `none`. The state template must not render to a string, including `unknown` or `unavailable`. An `availability` template may be defined to suppress rendering of the state template.",
"unit_of_measurement": "Defines the unit of measurement for the sensor, if any. This will also display the value based on the number format setting in the user profile and influence the graphical presentation in the history visualization as a continuous value."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -307,13 +380,18 @@
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]",
"value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful."
"value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful.",
"turn_off": "Defines actions to run when the switch is turned off.",
"turn_on": "Defines actions to run when the switch is turned on."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -324,24 +402,37 @@
"device_id": "[%key:common::config_flow::data::device%]",
"name": "[%key:common::config_flow::data::name%]",
"state": "[%key:component::template::common::state%]",
"start": "Actions on turn off",
"start": "Actions on start",
"fan_speed": "Fan speed",
"fan_speeds": "Fan speeds",
"set_fan_speed": "Actions on set fan speed",
"stop": "Actions on stop",
"pause": "Actions on pause",
"return_to_base": "Actions on return to base",
"return_to_base": "Actions on return to dock",
"clean_spot": "Actions on clean spot",
"locate": "Actions on locate"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "Defines a template to get the state of the vacuum. Valid values are `cleaning`, `docked`, `idle`, `paused`, `returning`, and `error`.",
"start": "Defines actions to run when the vacuum is started.",
"fan_speed": "Defines a template to get the fan speed of the vacuum.",
"fan_speeds": "List of fan speeds supported by the vacuum.",
"set_fan_speed": "Defines actions to run when the vacuum is given a command to set the fan speed. Receives variable `fan_speed`.",
"stop": "Defines actions to run when the vacuum is stopped.",
"pause": "Defines actions to run when the vacuum is paused.",
"return_to_base": "Defines actions to run when the vacuum is given a 'Return to dock' command.",
"clean_spot": "Defines actions to run when the vacuum is given a 'Clean spot' command.",
"locate": "Defines actions to run when the vacuum is given a 'Locate' command."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -349,6 +440,12 @@
}
}
},
"issues": {
"deprecated_battery_level": {
"title": "Deprecated battery level option in {entity_name}",
"description": "The template vacuum options `battery_level` and `battery_level_template` are being removed in 2026.8.\n\nPlease remove the `battery_level` or `battery_level_template` option from the YAML configuration for {entity_id} ({entity_name})."
}
},
"options": {
"step": {
"alarm_control_panel": {
@@ -366,13 +463,26 @@
"code_format": "[%key:component::template::common::code_format%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"value_template": "[%key:component::template::config::step::alarm_control_panel::data_description::value_template%]",
"disarm": "[%key:component::template::config::step::alarm_control_panel::data_description::disarm%]",
"arm_away": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_away%]",
"arm_custom_bypass": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_custom_bypass%]",
"arm_home": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_home%]",
"arm_night": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_night%]",
"arm_vacation": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_vacation%]",
"trigger": "[%key:component::template::config::step::alarm_control_panel::data_description::trigger%]",
"code_arm_required": "[%key:component::template::config::step::alarm_control_panel::data_description::code_arm_required%]",
"code_format": "[%key:component::template::config::step::alarm_control_panel::data_description::code_format%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -384,13 +494,17 @@
"state": "[%key:component::template::common::state%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "[%key:component::template::config::step::binary_sensor::data_description::state%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -402,13 +516,17 @@
"press": "[%key:component::template::config::step::button::data::press%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"press": "[%key:component::template::config::step::button::data_description::press%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -439,6 +557,9 @@
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -468,6 +589,9 @@
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -480,13 +604,18 @@
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"url": "[%key:component::template::config::step::image::data_description::url%]",
"verify_ssl": "[%key:component::template::config::step::image::data_description::verify_ssl%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -507,13 +636,25 @@
"set_temperature": "[%key:component::template::config::step::light::data::set_temperature%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "[%key:component::template::config::step::light::data_description::state%]",
"turn_off": "[%key:component::template::config::step::light::data_description::turn_off%]",
"turn_on": "[%key:component::template::config::step::light::data_description::turn_on%]",
"level": "[%key:component::template::config::step::light::data_description::level%]",
"set_level": "[%key:component::template::config::step::light::data_description::set_level%]",
"hs": "[%key:component::template::config::step::light::data_description::hs%]",
"set_hs": "[%key:component::template::config::step::light::data_description::set_hs%]",
"temperature": "[%key:component::template::config::step::light::data_description::temperature%]",
"set_temperature": "[%key:component::template::config::step::light::data_description::set_temperature%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -529,13 +670,21 @@
"open": "[%key:component::template::config::step::lock::data::open%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "[%key:component::template::config::step::lock::data_description::state%]",
"lock": "[%key:component::template::config::step::lock::data_description::lock%]",
"unlock": "[%key:component::template::config::step::lock::data_description::unlock%]",
"code_format": "[%key:component::template::config::step::lock::data_description::code_format%]",
"open": "[%key:component::template::config::step::lock::data_description::open%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -552,13 +701,21 @@
"min": "[%key:component::template::config::step::number::data::min%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "[%key:component::template::config::step::number::data_description::state%]",
"step": "[%key:component::template::config::step::number::data_description::step%]",
"set_value": "[%key:component::template::config::step::number::data_description::set_value%]",
"max": "[%key:component::template::config::step::number::data_description::max%]",
"min": "[%key:component::template::config::step::number::data_description::min%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -573,13 +730,19 @@
"options": "[%key:component::template::config::step::select::data::options%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "[%key:component::template::config::step::select::data_description::state%]",
"select_option": "[%key:component::template::config::step::select::data_description::select_option%]",
"options": "[%key:component::template::config::step::select::data_description::options%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -594,13 +757,18 @@
"unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "[%key:component::template::config::step::sensor::data_description::state%]",
"unit_of_measurement": "[%key:component::template::config::step::sensor::data_description::state%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -616,13 +784,18 @@
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]",
"value_template": "[%key:component::template::config::step::switch::data_description::value_template%]"
"value_template": "[%key:component::template::config::step::switch::data_description::value_template%]",
"turn_off": "[%key:component::template::config::step::switch::data_description::turn_off%]",
"turn_on": "[%key:component::template::config::step::switch::data_description::turn_on%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
@@ -644,17 +817,30 @@
"locate": "[%key:component::template::config::step::vacuum::data::locate%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]"
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "[%key:component::template::config::step::vacuum::data_description::state%]",
"start": "[%key:component::template::config::step::vacuum::data_description::start%]",
"fan_speed": "[%key:component::template::config::step::vacuum::data_description::fan_speed%]",
"fan_speeds": "[%key:component::template::config::step::vacuum::data_description::fan_speeds%]",
"set_fan_speed": "[%key:component::template::config::step::vacuum::data_description::set_fan_speed%]",
"stop": "[%key:component::template::config::step::vacuum::data_description::stop%]",
"pause": "[%key:component::template::config::step::vacuum::data_description::pause%]",
"return_to_base": "[%key:component::template::config::step::vacuum::data_description::return_to_base%]",
"clean_spot": "[%key:component::template::config::step::vacuum::data_description::clean_spot%]",
"locate": "[%key:component::template::config::step::vacuum::data_description::locate%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
},
"data_description": {
"availability": "[%key:component::template::common::availability_description%]"
}
}
},
"title": "Template vacuum"
"title": "[%key:component::template::config::step::vacuum::title%]"
}
}
},
@@ -721,6 +907,7 @@
},
"sensor_device_class": {
"options": {
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
"area": "[%key:component::sensor::entity_component::area::name%]",
@@ -768,7 +955,7 @@
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]",
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
"volume": "[%key:component::sensor::entity_component::volume::name%]",
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",

View File

@@ -34,11 +34,16 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers import (
config_validation as cv,
issue_registry as ir,
template,
)
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN
@@ -188,6 +193,26 @@ def async_create_preview_vacuum(
)
def create_issue(
hass: HomeAssistant, supported_features: int, name: str, entity_id: str
) -> None:
"""Create the battery_level issue."""
if supported_features & VacuumEntityFeature.BATTERY:
key = "deprecated_battery_level"
ir.async_create_issue(
hass,
DOMAIN,
f"{key}_{entity_id}",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=key,
translation_placeholders={
"entity_name": name,
"entity_id": entity_id,
},
)
class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
"""Representation of a template vacuum features."""
@@ -369,6 +394,16 @@ class TemplateStateVacuumEntity(TemplateEntity, AbstractTemplateVacuum):
self.add_script(action_id, action_config, name, DOMAIN)
self._attr_supported_features |= supported_feature
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
create_issue(
self.hass,
self._attr_supported_features,
self._attr_name or DEFAULT_NAME,
self.entity_id,
)
@callback
def _async_setup_templates(self) -> None:
"""Set up templates."""
@@ -434,6 +469,16 @@ class TriggerVacuumEntity(TriggerEntity, AbstractTemplateVacuum):
self._to_render_simple.append(key)
self._parse_result.add(key)
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
create_issue(
self.hass,
self._attr_supported_features,
self._attr_name or DEFAULT_NAME,
self.entity_id,
)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle update of the data."""

View File

@@ -34,6 +34,7 @@ from homeassistant.components.weather import (
from homeassistant.const import (
CONF_NAME,
CONF_TEMPERATURE_UNIT,
CONF_UNIQUE_ID,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
@@ -151,6 +152,7 @@ PLATFORM_SCHEMA = vol.Schema(
vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS),
vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template,
vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS),
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template,
vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template,

View File

@@ -28,7 +28,7 @@ class TeslemetryData:
vehicles: list[TeslemetryVehicleData]
energysites: list[TeslemetryEnergyData]
scopes: list[Scope]
stream: TeslemetryStream
stream: TeslemetryStream | None
@dataclass

View File

@@ -45,7 +45,7 @@ from .entity import (
TeslemetryVehicleStreamEntity,
TeslemetryWallConnectorEntity,
)
from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData
from .models import TeslemetryEnergyData, TeslemetryVehicleData
PARALLEL_UPDATES = 0
@@ -1617,11 +1617,12 @@ async def async_setup_entry(
if energysite.history_coordinator is not None
)
entities.append(
TeslemetryCreditBalanceSensor(
entry.unique_id or entry.entry_id, entry.runtime_data
if entry.runtime_data.stream is not None:
entities.append(
TeslemetryCreditBalanceSensor(
entry.unique_id or entry.entry_id, entry.runtime_data.stream
)
)
)
async_add_entities(entities)
@@ -1840,12 +1841,12 @@ class TeslemetryCreditBalanceSensor(RestoreSensor):
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_suggested_display_precision = 0
def __init__(self, uid: str, data: TeslemetryData) -> None:
def __init__(self, uid: str, stream: TeslemetryStream) -> None:
"""Initialize common aspects of a Teslemetry entity."""
self._attr_translation_key = "credit_balance"
self._attr_unique_id = f"{uid}_credit_balance"
self.stream = data.stream
self.stream = stream
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""

View File

@@ -267,7 +267,9 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
return int(self._speed.remap_value_to(value, 1, 100))
if self._speeds is not None:
if (value := self.device.status.get(self._speeds.dpcode)) is None:
if (
value := self.device.status.get(self._speeds.dpcode)
) is None or value not in self._speeds.range:
return None
return ordered_list_item_to_percentage(self._speeds.range, value)

View File

@@ -16,6 +16,7 @@ from homeassistant.components.light import (
ColorMode,
LightEntity,
LightEntityDescription,
color_supported,
filter_supported_color_modes,
)
from homeassistant.const import EntityCategory
@@ -530,19 +531,6 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
description.brightness_min, dptype=DPType.INTEGER
)
if int_type := self.find_dpcode(
description.color_temp, dptype=DPType.INTEGER, prefer_function=True
):
self._color_temp = int_type
color_modes.add(ColorMode.COLOR_TEMP)
# If entity does not have color_temp, check if it has work_mode "white"
elif color_mode_enum := self.find_dpcode(
description.color_mode, dptype=DPType.ENUM, prefer_function=True
):
if WorkMode.WHITE.value in color_mode_enum.range:
color_modes.add(ColorMode.WHITE)
self._white_color_mode = ColorMode.WHITE
if (
dpcode := self.find_dpcode(description.color_data, prefer_function=True)
) and self.get_dptype(dpcode) == DPType.JSON:
@@ -568,6 +556,26 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
):
self._color_data_type = DEFAULT_COLOR_TYPE_DATA_V2
# Check if the light has color temperature
if int_type := self.find_dpcode(
description.color_temp, dptype=DPType.INTEGER, prefer_function=True
):
self._color_temp = int_type
color_modes.add(ColorMode.COLOR_TEMP)
# If light has color but does not have color_temp, check if it has
# work_mode "white"
elif (
color_supported(color_modes)
and (
color_mode_enum := self.find_dpcode(
description.color_mode, dptype=DPType.ENUM, prefer_function=True
)
)
and WorkMode.WHITE.value in color_mode_enum.range
):
color_modes.add(ColorMode.WHITE)
self._white_color_mode = ColorMode.WHITE
self._attr_supported_color_modes = filter_supported_color_modes(color_modes)
if len(self._attr_supported_color_modes) == 1:
# If the light supports only a single color mode, set it now

View File

@@ -25,6 +25,7 @@ from ..const import LOGGER, UNIFI_WIRELESS_CLIENTS
from ..entity import UnifiEntity, UnifiEntityDescription
if TYPE_CHECKING:
from .. import UnifiConfigEntry
from .hub import UnifiHub
CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1)
@@ -34,7 +35,7 @@ POLL_INTERVAL = timedelta(seconds=10)
class UnifiEntityLoader:
"""UniFi Network integration handling platforms for entity registration."""
def __init__(self, hub: UnifiHub) -> None:
def __init__(self, hub: UnifiHub, config_entry: UnifiConfigEntry) -> None:
"""Initialize the UniFi entity loader."""
self.hub = hub
self.api_updaters = (
@@ -57,15 +58,16 @@ class UnifiEntityLoader:
)
self.wireless_clients = hub.hass.data[UNIFI_WIRELESS_CLIENTS]
self._dataUpdateCoordinator = DataUpdateCoordinator(
self._data_update_coordinator = DataUpdateCoordinator(
hub.hass,
LOGGER,
name="Unifi entity poller",
config_entry=config_entry,
update_method=self._update_pollable_api_data,
update_interval=POLL_INTERVAL,
)
self._update_listener = self._dataUpdateCoordinator.async_add_listener(
self._update_listener = self._data_update_coordinator.async_add_listener(
update_callback=lambda: None
)

View File

@@ -39,7 +39,7 @@ class UnifiHub:
self.hass = hass
self.api = api
self.config = UnifiConfig.from_config_entry(config_entry)
self.entity_loader = UnifiEntityLoader(self)
self.entity_loader = UnifiEntityLoader(self, config_entry)
self._entity_helper = UnifiEntityHelper(hass, api)
self.websocket = UnifiWebsocket(hass, api, self.signal_reachable)

View File

@@ -162,7 +162,11 @@ class UptimeKumaSensorEntity(
name=coordinator.data[monitor].monitor_name,
identifiers={(DOMAIN, f"{coordinator.config_entry.entry_id}_{monitor!s}")},
manufacturer="Uptime Kuma",
configuration_url=coordinator.config_entry.data[CONF_URL],
configuration_url=(
None
if "127.0.0.1" in (url := coordinator.config_entry.data[CONF_URL])
else url
),
sw_version=coordinator.api.version.version,
)

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["voip_utils"],
"quality_scale": "internal",
"requirements": ["voip-utils==0.3.3"]
"requirements": ["voip-utils==0.3.4"]
}

View File

@@ -9,6 +9,7 @@ from typing import Any
import voluptuous as vol
from volvocarsapi.api import VolvoCarsApi
from volvocarsapi.models import VolvoApiException, VolvoCarsVehicle
from volvocarsapi.scopes import DEFAULT_SCOPES
from homeassistant.config_entries import (
SOURCE_REAUTH,
@@ -54,6 +55,13 @@ class VolvoOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
self._vehicles: list[VolvoCarsVehicle] = []
self._config_data: dict = {}
@property
def extra_authorize_data(self) -> dict:
"""Extra data that needs to be appended to the authorize url."""
return super().extra_authorize_data | {
"scope": " ".join(DEFAULT_SCOPES),
}
@property
def logger(self) -> logging.Logger:
"""Return logger."""

View File

@@ -20,7 +20,11 @@
"default": "mdi:gas-station"
},
"charger_connection_status": {
"default": "mdi:ev-plug-ccs2"
"default": "mdi:power-plug-off",
"state": {
"connected": "mdi:power-plug",
"fault": "mdi:flash-alert"
}
},
"charging_power": {
"default": "mdi:gauge-empty",
@@ -44,22 +48,22 @@
}
},
"distance_to_empty_battery": {
"default": "mdi:gauge-empty"
"default": "mdi:battery-outline"
},
"distance_to_empty_tank": {
"default": "mdi:gauge-empty"
},
"distance_to_service": {
"default": "mdi:wrench-clock"
"default": "mdi:wrench-check"
},
"engine_time_to_service": {
"default": "mdi:wrench-clock"
"default": "mdi:wrench-cog"
},
"estimated_charging_time": {
"default": "mdi:battery-clock"
},
"fuel_amount": {
"default": "mdi:gas-station"
"default": "mdi:fuel"
},
"odometer": {
"default": "mdi:counter"

View File

@@ -13,6 +13,6 @@
"documentation": "https://www.home-assistant.io/integrations/wyoming",
"integration_type": "service",
"iot_class": "local_push",
"requirements": ["wyoming==1.7.1"],
"requirements": ["wyoming==1.7.2"],
"zeroconf": ["_wyoming._tcp.local."]
}

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/yale",
"iot_class": "cloud_push",
"loggers": ["socketio", "engineio", "yalexs"],
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"]
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"]
}

View File

@@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"iot_class": "local_push",
"requirements": ["yalexs-ble==3.1.0"]
"requirements": ["yalexs-ble==3.1.2"]
}

View File

@@ -74,7 +74,12 @@ from zha.event import EventBase
from zha.exceptions import ZHAException
from zha.mixins import LogMixin
from zha.zigbee.cluster_handlers import ClusterBindEvent, ClusterConfigureReportingEvent
from zha.zigbee.device import ClusterHandlerConfigurationComplete, Device, ZHAEvent
from zha.zigbee.device import (
ClusterHandlerConfigurationComplete,
Device,
DeviceFirmwareInfoUpdatedEvent,
ZHAEvent,
)
from zha.zigbee.group import Group, GroupInfo, GroupMember
from zigpy.config import (
CONF_DATABASE,
@@ -843,8 +848,23 @@ class ZHAGatewayProxy(EventBase):
name=zha_device.name,
manufacturer=zha_device.manufacturer,
model=zha_device.model,
sw_version=zha_device.firmware_version,
)
zha_device_proxy.device_id = device_registry_device.id
def update_sw_version(event: DeviceFirmwareInfoUpdatedEvent) -> None:
"""Update software version in device registry."""
device_registry.async_update_device(
device_registry_device.id,
sw_version=event.new_firmware_version,
)
self._unsubs.append(
zha_device.on_event(
DeviceFirmwareInfoUpdatedEvent.event_type, update_sw_version
)
)
return zha_device_proxy
def _async_get_or_create_group_proxy(self, group_info: GroupInfo) -> ZHAGroupProxy:

View File

@@ -21,7 +21,7 @@
"zha",
"universal_silabs_flasher"
],
"requirements": ["zha==0.0.62"],
"requirements": ["zha==0.0.65"],
"usb": [
{
"vid": "10C4",

View File

@@ -616,6 +616,18 @@
},
"water_supply": {
"name": "Water supply"
},
"frient_in_1": {
"name": "IN1"
},
"frient_in_2": {
"name": "IN2"
},
"frient_in_3": {
"name": "IN3"
},
"frient_in_4": {
"name": "IN4"
}
},
"button": {
@@ -639,6 +651,9 @@
},
"frost_lock_reset": {
"name": "Frost lock reset"
},
"reset_alarm": {
"name": "Reset alarm"
}
},
"climate": {
@@ -1472,6 +1487,9 @@
"tier6_summation_delivered": {
"name": "Tier 6 summation delivered"
},
"total_active_power": {
"name": "Total power"
},
"summation_received": {
"name": "Summation received"
},
@@ -2006,6 +2024,18 @@
},
"auto_relock": {
"name": "Autorelock"
},
"distance_tracking": {
"name": "Distance tracking"
},
"water_shortage_auto_close": {
"name": "Water shortage auto-close"
},
"frient_com_1": {
"name": "COM 1"
},
"frient_com_2": {
"name": "COM 2"
}
}
}

View File

@@ -58,7 +58,7 @@ async def async_setup_entry(
zha_data = get_zha_data(hass)
if zha_data.update_coordinator is None:
zha_data.update_coordinator = ZHAFirmwareUpdateCoordinator(
hass, get_zha_gateway(hass).application_controller
hass, config_entry, get_zha_gateway(hass).application_controller
)
entities_to_create = zha_data.platforms[Platform.UPDATE]
@@ -79,12 +79,16 @@ class ZHAFirmwareUpdateCoordinator(DataUpdateCoordinator[None]): # pylint: disa
"""Firmware update coordinator that broadcasts updates network-wide."""
def __init__(
self, hass: HomeAssistant, controller_application: ControllerApplication
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
controller_application: ControllerApplication,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name="ZHA firmware update coordinator",
update_method=self.async_update_data,
)

View File

@@ -105,7 +105,6 @@ from .const import (
CONF_USB_PATH,
CONF_USE_ADDON,
DOMAIN,
DRIVER_READY_TIMEOUT,
EVENT_DEVICE_ADDED_TO_REGISTRY,
EVENT_VALUE_UPDATED,
LIB_LOGGER,
@@ -136,6 +135,7 @@ from .models import ZwaveJSConfigEntry, ZwaveJSData
from .services import async_setup_services
CONNECT_TIMEOUT = 10
DRIVER_READY_TIMEOUT = 60
CONFIG_SCHEMA = vol.Schema(
{
@@ -368,6 +368,16 @@ class DriverEvents:
)
)
# listen for driver ready event to reload the config entry
self.config_entry.async_on_unload(
driver.on(
"driver ready",
lambda _: self.hass.config_entries.async_schedule_reload(
self.config_entry.entry_id
),
)
)
# listen for new nodes being added to the mesh
self.config_entry.async_on_unload(
controller.on(
@@ -499,7 +509,7 @@ class ControllerEvents:
)
)
await self.async_check_preprovisioned_device(node)
await self.async_check_pre_provisioned_device(node)
if node.is_controller_node:
# Create a controller status sensor for each device
@@ -627,8 +637,8 @@ class ControllerEvents:
f"{DOMAIN}.identify_controller.{dev_id[1]}",
)
async def async_check_preprovisioned_device(self, node: ZwaveNode) -> None:
"""Check if the node was preprovisioned and update the device registry."""
async def async_check_pre_provisioned_device(self, node: ZwaveNode) -> None:
"""Check if the node was pre-provisioned and update the device registry."""
provisioning_entry = (
await self.driver_events.driver.controller.async_get_provisioning_entry(
node.node_id
@@ -638,29 +648,37 @@ class ControllerEvents:
provisioning_entry
and provisioning_entry.additional_properties
and "device_id" in provisioning_entry.additional_properties
):
preprovisioned_device = self.dev_reg.async_get(
provisioning_entry.additional_properties["device_id"]
and (
pre_provisioned_device := self.dev_reg.async_get(
provisioning_entry.additional_properties["device_id"]
)
)
and (dsk_identifier := (DOMAIN, f"provision_{provisioning_entry.dsk}"))
in pre_provisioned_device.identifiers
):
driver = self.driver_events.driver
device_id = get_device_id(driver, node)
device_id_ext = get_device_id_ext(driver, node)
new_identifiers = pre_provisioned_device.identifiers.copy()
new_identifiers.remove(dsk_identifier)
new_identifiers.add(device_id)
if device_id_ext:
new_identifiers.add(device_id_ext)
if preprovisioned_device:
dsk = provisioning_entry.dsk
dsk_identifier = (DOMAIN, f"provision_{dsk}")
# If the pre-provisioned device has the DSK identifier, remove it
if dsk_identifier in preprovisioned_device.identifiers:
driver = self.driver_events.driver
device_id = get_device_id(driver, node)
device_id_ext = get_device_id_ext(driver, node)
new_identifiers = preprovisioned_device.identifiers.copy()
new_identifiers.remove(dsk_identifier)
new_identifiers.add(device_id)
if device_id_ext:
new_identifiers.add(device_id_ext)
self.dev_reg.async_update_device(
preprovisioned_device.id,
new_identifiers=new_identifiers,
)
if self.dev_reg.async_get_device(identifiers=new_identifiers):
# If a device entry is registered with the node ID based identifiers,
# just remove the device entry with the DSK identifier.
self.dev_reg.async_update_device(
pre_provisioned_device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
else:
# Add the node ID based identifiers to the device entry
# with the DSK identifier and remove the DSK identifier.
self.dev_reg.async_update_device(
pre_provisioned_device.id,
new_identifiers=new_identifiers,
)
async def async_register_node_in_dev_reg(self, node: ZwaveNode) -> dr.DeviceEntry:
"""Register node in dev reg."""
@@ -1074,23 +1092,32 @@ async def client_listen(
try:
await client.listen(driver_ready)
except BaseZwaveJSServerError as err:
if entry.state is not ConfigEntryState.LOADED:
if entry.state is ConfigEntryState.SETUP_IN_PROGRESS:
raise
LOGGER.error("Client listen failed: %s", err)
except Exception as err:
# We need to guard against unknown exceptions to not crash this task.
LOGGER.exception("Unexpected exception: %s", err)
if entry.state is not ConfigEntryState.LOADED:
if entry.state is ConfigEntryState.SETUP_IN_PROGRESS:
raise
if hass.is_stopping or entry.state is ConfigEntryState.UNLOAD_IN_PROGRESS:
return
if entry.state is ConfigEntryState.SETUP_IN_PROGRESS:
raise HomeAssistantError("Listen task ended unexpectedly")
# The entry needs to be reloaded since a new driver state
# will be acquired on reconnect.
# All model instances will be replaced when the new state is acquired.
if not hass.is_stopping:
if entry.state is not ConfigEntryState.LOADED:
raise HomeAssistantError("Listen task ended unexpectedly")
if entry.state.recoverable:
LOGGER.debug("Disconnected from server. Reloading integration")
hass.config_entries.async_schedule_reload(entry.entry_id)
else:
LOGGER.error(
"Disconnected from server. Cannot recover entry %s",
entry.title,
)
async def async_unload_entry(hass: HomeAssistant, entry: ZwaveJSConfigEntry) -> bool:

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine
from contextlib import suppress
import dataclasses
@@ -87,7 +86,6 @@ from .const import (
CONF_DATA_COLLECTION_OPTED_IN,
CONF_INSTALLER_MODE,
DOMAIN,
DRIVER_READY_TIMEOUT,
EVENT_DEVICE_ADDED_TO_REGISTRY,
LOGGER,
USER_AGENT,
@@ -98,6 +96,7 @@ from .helpers import (
async_get_node_from_device_id,
async_get_provisioning_entry_from_device_id,
async_get_version_info,
async_wait_for_driver_ready_event,
get_device_id,
)
@@ -2854,26 +2853,18 @@ async def websocket_hard_reset_controller(
connection.send_result(msg[ID], device.id)
async_cleanup()
@callback
def set_driver_ready(event: dict) -> None:
"Set the driver ready event."
wait_driver_ready.set()
wait_driver_ready = asyncio.Event()
msg[DATA_UNSUBSCRIBE] = unsubs = [
async_dispatcher_connect(
hass, EVENT_DEVICE_ADDED_TO_REGISTRY, _handle_device_added
),
driver.once("driver ready", set_driver_ready),
]
wait_for_driver_ready = async_wait_for_driver_ready_event(entry, driver)
await driver.async_hard_reset()
with suppress(TimeoutError):
async with asyncio.timeout(DRIVER_READY_TIMEOUT):
await wait_driver_ready.wait()
await wait_for_driver_ready()
# When resetting the controller, the controller home id is also changed.
# The controller state in the client is stale after resetting the controller,
# so get the new home id with a new client using the helper function.
@@ -2886,14 +2877,14 @@ async def websocket_hard_reset_controller(
# The stale unique id needs to be handled by a repair flow,
# after the config entry has been reloaded.
LOGGER.error(
"Failed to get server version, cannot update config entry"
"Failed to get server version, cannot update config entry "
"unique id with new home id, after controller reset"
)
else:
hass.config_entries.async_update_entry(
entry, unique_id=str(version_info.home_id)
)
await hass.config_entries.async_reload(entry.entry_id)
hass.config_entries.async_schedule_reload(entry.entry_id)
@websocket_api.websocket_command(
@@ -3100,27 +3091,19 @@ async def websocket_restore_nvm(
)
)
@callback
def set_driver_ready(event: dict) -> None:
"Set the driver ready event."
wait_driver_ready.set()
wait_driver_ready = asyncio.Event()
# Set up subscription for progress events
connection.subscriptions[msg["id"]] = async_cleanup
msg[DATA_UNSUBSCRIBE] = unsubs = [
controller.on("nvm convert progress", forward_progress),
controller.on("nvm restore progress", forward_progress),
driver.once("driver ready", set_driver_ready),
]
wait_for_driver_ready = async_wait_for_driver_ready_event(entry, driver)
await controller.async_restore_nvm_base64(msg["data"], {"preserveRoutes": False})
with suppress(TimeoutError):
async with asyncio.timeout(DRIVER_READY_TIMEOUT):
await wait_driver_ready.wait()
await wait_for_driver_ready()
# When restoring the NVM to the controller, the controller home id is also changed.
# The controller state in the client is stale after restoring the NVM,
# so get the new home id with a new client using the helper function.
@@ -3133,14 +3116,13 @@ async def websocket_restore_nvm(
# The stale unique id needs to be handled by a repair flow,
# after the config entry has been reloaded.
LOGGER.error(
"Failed to get server version, cannot update config entry"
"Failed to get server version, cannot update config entry "
"unique id with new home id, after controller NVM restore"
)
else:
hass.config_entries.async_update_entry(
entry, unique_id=str(version_info.home_id)
)
await hass.config_entries.async_reload(entry.entry_id)
connection.send_message(
@@ -3152,3 +3134,4 @@ async def websocket_restore_nvm(
)
)
connection.send_result(msg[ID])
async_cleanup()

View File

@@ -62,9 +62,12 @@ from .const import (
CONF_USB_PATH,
CONF_USE_ADDON,
DOMAIN,
DRIVER_READY_TIMEOUT,
)
from .helpers import CannotConnect, async_get_version_info
from .helpers import (
CannotConnect,
async_get_version_info,
async_wait_for_driver_ready_event,
)
from .models import ZwaveJSConfigEntry
_LOGGER = logging.getLogger(__name__)
@@ -90,6 +93,10 @@ MIN_MIGRATION_SDK_VERSION = AwesomeVersion("6.61")
NETWORK_TYPE_NEW = "new"
NETWORK_TYPE_EXISTING = "existing"
ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS = (
"https://www.home-assistant.io/integrations/zwave_js/"
"#how-to-migrate-from-one-adapter-to-a-new-adapter-using-z-wave-js-ui"
)
def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema:
@@ -443,7 +450,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
None,
)
if not self._reconfigure_config_entry:
return self.async_abort(reason="addon_required")
return self.async_abort(
reason="addon_required",
description_placeholders={
"zwave_js_ui_migration": ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS,
},
)
vid = discovery_info.vid
pid = discovery_info.pid
@@ -887,7 +899,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
config_entry = self._reconfigure_config_entry
assert config_entry is not None
if not self._usb_discovery and not config_entry.data.get(CONF_USE_ADDON):
return self.async_abort(reason="addon_required")
return self.async_abort(
reason="addon_required",
description_placeholders={
"zwave_js_ui_migration": ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS,
},
)
try:
driver = self._get_driver()
@@ -1396,19 +1413,15 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
event["bytesWritten"] / event["total"] * 0.5 + 0.5
)
@callback
def set_driver_ready(event: dict) -> None:
"Set the driver ready event."
wait_driver_ready.set()
driver = self._get_driver()
controller = driver.controller
wait_driver_ready = asyncio.Event()
unsubs = [
controller.on("nvm convert progress", forward_progress),
controller.on("nvm restore progress", forward_progress),
driver.once("driver ready", set_driver_ready),
]
wait_for_driver_ready = async_wait_for_driver_ready_event(config_entry, driver)
try:
await controller.async_restore_nvm(
self.backup_data, {"preserveRoutes": False}
@@ -1417,8 +1430,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
raise AbortFlow(f"Failed to restore network: {err}") from err
else:
with suppress(TimeoutError):
async with asyncio.timeout(DRIVER_READY_TIMEOUT):
await wait_driver_ready.wait()
await wait_for_driver_ready()
try:
version_info = await async_get_version_info(
self.hass, config_entry.data[CONF_URL]
@@ -1435,10 +1447,10 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
self.hass.config_entries.async_update_entry(
config_entry, unique_id=str(version_info.home_id)
)
await self.hass.config_entries.async_reload(config_entry.entry_id)
# Reload the config entry two times to clean up
# the stale device entry.
# The config entry will be also be reloaded when the driver is ready,
# by the listener in the package module,
# and two reloads are needed to clean up the stale controller device entry.
# Since both the old and the new controller have the same node id,
# but different hardware identifiers, the integration
# will create a new device for the new controller, on the first reload,

View File

@@ -201,7 +201,3 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = {
WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE,
WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION,
}
# Other constants
DRIVER_READY_TIMEOUT = 60

View File

@@ -3,7 +3,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
from collections.abc import Callable, Coroutine
from dataclasses import astuple, dataclass
import logging
from typing import Any, cast
@@ -56,6 +56,7 @@ from .const import (
)
from .models import ZwaveJSConfigEntry
DRIVER_READY_EVENT_TIMEOUT = 60
SERVER_VERSION_TIMEOUT = 10
@@ -588,5 +589,57 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio
return version_info
@callback
def async_wait_for_driver_ready_event(
config_entry: ZwaveJSConfigEntry,
driver: Driver,
) -> Callable[[], Coroutine[Any, Any, None]]:
"""Wait for the driver ready event and the config entry reload.
When the driver ready event is received
the config entry will be reloaded by the integration.
This function helps wait for that to happen
before proceeding with further actions.
If the config entry is reloaded for another reason,
this function will not wait for it to be reloaded again.
Raises TimeoutError if the driver ready event and reload
is not received within the specified timeout.
"""
driver_ready_event_received = asyncio.Event()
config_entry_reloaded = asyncio.Event()
unsubscribers: list[Callable[[], None]] = []
@callback
def driver_ready_received(event: dict) -> None:
"""Receive the driver ready event."""
driver_ready_event_received.set()
unsubscribers.append(driver.once("driver ready", driver_ready_received))
@callback
def on_config_entry_state_change() -> None:
"""Check config entry was loaded after driver ready event."""
if config_entry.state is ConfigEntryState.LOADED:
config_entry_reloaded.set()
unsubscribers.append(
config_entry.async_on_state_change(on_config_entry_state_change)
)
async def wait_for_events() -> None:
try:
async with asyncio.timeout(DRIVER_READY_EVENT_TIMEOUT):
await asyncio.gather(
driver_ready_event_received.wait(), config_entry_reloaded.wait()
)
finally:
for unsubscribe in unsubscribers:
unsubscribe()
return wait_for_events
class CannotConnect(HomeAssistantError):
"""Indicate connection error."""

View File

@@ -9,7 +9,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["zwave_js_server"],
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.67.0"],
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.67.1"],
"usb": [
{
"vid": "0658",

View File

@@ -4,7 +4,7 @@
"addon_get_discovery_info_failed": "Failed to get Z-Wave add-on discovery info.",
"addon_info_failed": "Failed to get Z-Wave add-on info.",
"addon_install_failed": "Failed to install the Z-Wave add-on.",
"addon_required": "The Z-Wave migration flow requires the integration to be configured using the Z-Wave Supervisor add-on. You can still use the Backup and Restore buttons to migrate your network manually.",
"addon_required": "The Z-Wave migration flow requires the integration to be configured using the Z-Wave Supervisor add-on. If you are using Z-Wave JS UI, please follow our [migration instructions]({zwave_js_ui_migration}).",
"addon_set_config_failed": "Failed to set Z-Wave configuration.",
"addon_start_failed": "Failed to start the Z-Wave add-on.",
"addon_stop_failed": "Failed to stop the Z-Wave add-on.",

View File

@@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "0.dev0"
PATCH_VERSION: Final = "0b3"
__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

@@ -677,9 +677,10 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]):
and key in suggested_values
):
new_section_key = copy.copy(key)
schema[new_section_key] = val
val.schema = self.add_suggested_values_to_schema(
val.schema, suggested_values[key]
new_val = copy.copy(val)
schema[new_section_key] = new_val
new_val.schema = self.add_suggested_values_to_schema(
new_val.schema, suggested_values[key]
)
continue

View File

@@ -32,6 +32,7 @@ from homeassistant.util.json import format_unserializable_data
from . import storage, translation
from .debounce import Debouncer
from .deprecation import deprecated_function
from .frame import ReportBehavior, report_usage
from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment
from .registry import BaseRegistry, BaseRegistryItems, RegistryIndexType
@@ -67,6 +68,7 @@ CONNECTION_ZIGBEE = "zigbee"
ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30
# Can be removed when suggested_area is removed from DeviceEntry
RUNTIME_ONLY_ATTRS = {"suggested_area"}
CONFIGURATION_URL_SCHEMES = {"http", "https", "homeassistant"}
@@ -156,7 +158,7 @@ class _EventDeviceRegistryUpdatedData_Remove(TypedDict):
action: Literal["remove"]
device_id: str
device: DeviceEntry
device: dict[str, Any]
class _EventDeviceRegistryUpdatedData_Update(TypedDict):
@@ -343,7 +345,8 @@ class DeviceEntry:
name: str | None = attr.ib(default=None)
primary_config_entry: str | None = attr.ib(default=None)
serial_number: str | None = attr.ib(default=None)
suggested_area: str | None = attr.ib(default=None)
# Suggested area is deprecated and will be removed from DeviceEntry in 2026.9.
_suggested_area: str | None = attr.ib(default=None)
sw_version: str | None = attr.ib(default=None)
via_device_id: str | None = attr.ib(default=None)
# This value is not stored, just used to keep track of events to fire.
@@ -442,6 +445,14 @@ class DeviceEntry:
)
)
@property
@deprecated_function(
"code which ignores suggested_area", breaks_in_ha_version="2026.9"
)
def suggested_area(self) -> str | None:
"""Return the suggested area for this device entry."""
return self._suggested_area
@attr.s(frozen=True, slots=True)
class DeletedDeviceEntry:
@@ -895,7 +906,19 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
if device is None:
deleted_device = self.deleted_devices.get_entry(identifiers, connections)
if deleted_device is None:
device = DeviceEntry(is_new=True)
area_id: str | None = None
if (
suggested_area is not None
and suggested_area is not UNDEFINED
and suggested_area != ""
):
# Circular dep
from . import area_registry as ar # noqa: PLC0415
area = ar.async_get(self.hass).async_get_or_create(suggested_area)
area_id = area.id
device = DeviceEntry(is_new=True, area_id=area_id)
else:
self.deleted_devices.pop(deleted_device.id)
device = deleted_device.to_device_entry(
@@ -950,7 +973,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
model_id=model_id,
name=name,
serial_number=serial_number,
suggested_area=suggested_area,
_suggested_area=suggested_area,
sw_version=sw_version,
via_device_id=via_device_id,
)
@@ -989,6 +1012,10 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
remove_config_entry_id: str | UndefinedType = UNDEFINED,
remove_config_subentry_id: str | None | UndefinedType = UNDEFINED,
serial_number: str | None | UndefinedType = UNDEFINED,
# _suggested_area is used internally by the device registry and must
# not be set by integrations.
_suggested_area: str | None | UndefinedType = UNDEFINED,
# suggested_area is deprecated and will be removed in 2026.9
suggested_area: str | None | UndefinedType = UNDEFINED,
sw_version: str | None | UndefinedType = UNDEFINED,
via_device_id: str | None | UndefinedType = UNDEFINED,
@@ -1054,19 +1081,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
"Cannot define both merge_identifiers and new_identifiers"
)
if (
suggested_area is not None
and suggested_area is not UNDEFINED
and suggested_area != ""
and area_id is UNDEFINED
and old.area_id is None
):
# Circular dep
from . import area_registry as ar # noqa: PLC0415
area = ar.async_get(self.hass).async_get_or_create(suggested_area)
area_id = area.id
if add_config_entry_id is not UNDEFINED:
if add_config_subentry_id is UNDEFINED:
# Interpret not specifying a subentry as None (the main entry)
@@ -1144,6 +1158,16 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
new_values["config_entries_subentries"] = config_entries_subentries
old_values["config_entries_subentries"] = old.config_entries_subentries
if suggested_area is not UNDEFINED:
report_usage(
"passes a suggested_area to device_registry.async_update device",
core_behavior=ReportBehavior.LOG,
breaks_in_ha_version="2026.9.0",
)
if _suggested_area is not UNDEFINED:
suggested_area = _suggested_area
added_connections: set[tuple[str, str]] | None = None
added_identifiers: set[tuple[str, str]] | None = None
@@ -1197,7 +1221,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
("name", name),
("name_by_user", name_by_user),
("serial_number", serial_number),
("suggested_area", suggested_area),
("sw_version", sw_version),
("via_device_id", via_device_id),
):
@@ -1205,12 +1228,18 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
new_values[attr_name] = value
old_values[attr_name] = getattr(old, attr_name)
# Can be removed when suggested_area is removed from DeviceEntry
if suggested_area is not UNDEFINED and suggested_area != old._suggested_area: # noqa: SLF001
new_values["suggested_area"] = suggested_area
old_values["suggested_area"] = old._suggested_area # noqa: SLF001
if old.is_new:
new_values["is_new"] = False
if not new_values:
return old
# This condition can be removed when suggested_area is removed from DeviceEntry
if not RUNTIME_ONLY_ATTRS.issuperset(new_values):
# Change modified_at if we are changing something that we store
new_values["modified_at"] = utcnow()
@@ -1233,6 +1262,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
# firing events for data we have nothing to compare
# against since its never saved on disk
if RUNTIME_ONLY_ATTRS.issuperset(new_values):
# This can be removed when suggested_area is removed from DeviceEntry
return new
self.async_schedule_save()
@@ -1319,7 +1349,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
self.hass.bus.async_fire_internal(
EVENT_DEVICE_REGISTRY_UPDATED,
_EventDeviceRegistryUpdatedData_Remove(
action="remove", device_id=device_id, device=device
action="remove", device_id=device_id, device=device.dict_repr
),
)
self.async_schedule_save()

View File

@@ -1103,13 +1103,13 @@ class EntityRegistry(BaseRegistry):
entities = async_entries_for_device(
self, event.data["device_id"], include_disabled_entities=True
)
removed_device = event.data["device"]
removed_device_dict = event.data["device"]
for entity in entities:
config_entry_id = entity.config_entry_id
if (
config_entry_id in removed_device.config_entries
config_entry_id in removed_device_dict["config_entries"]
and entity.config_subentry_id
in removed_device.config_entries_subentries[config_entry_id]
in removed_device_dict["config_entries_subentries"][config_entry_id]
):
self.async_remove(entity.entity_id)
else:

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