mirror of
https://github.com/home-assistant/core.git
synced 2025-09-20 02:19:36 +00:00
Compare commits
94 Commits
trigger_de
...
2025.8.0b3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
808273962d | ||
![]() |
094fe43557 | ||
![]() |
8f5bd51eef | ||
![]() |
faf0ded854 | ||
![]() |
d20302f97b | ||
![]() |
74c25496bc | ||
![]() |
67ecea0778 | ||
![]() |
164e5871cb | ||
![]() |
7a9966120e | ||
![]() |
d810b4ca38 | ||
![]() |
896062d669 | ||
![]() |
03bd133577 | ||
![]() |
4596c1644b | ||
![]() |
778fe96eb6 | ||
![]() |
a06557ed54 | ||
![]() |
641621d184 | ||
![]() |
b163f2b855 | ||
![]() |
0c0604e5bd | ||
![]() |
e0e4fc8afb | ||
![]() |
f832a2844f | ||
![]() |
4b0b268227 | ||
![]() |
dfc16d9f15 | ||
![]() |
4e3309bd22 | ||
![]() |
a5a45ce59f | ||
![]() |
6cb48da2f3 | ||
![]() |
ab5aac47b2 | ||
![]() |
d50b9405f0 | ||
![]() |
a2722f08c4 | ||
![]() |
aa700c3982 | ||
![]() |
3b1bb41129 | ||
![]() |
79ef51fb07 | ||
![]() |
53769da55e | ||
![]() |
82d153a240 | ||
![]() |
0dac635478 | ||
![]() |
90fc7d314b | ||
![]() |
636c1b7e4f | ||
![]() |
49c23de2d2 | ||
![]() |
e48820b2c1 | ||
![]() |
2b7a434677 | ||
![]() |
9ef7c6c99a | ||
![]() |
b789c11217 | ||
![]() |
5e8cd19cc3 | ||
![]() |
027052440d | ||
![]() |
47a7ed4084 | ||
![]() |
89f6cfeb81 | ||
![]() |
c268e57ba7 | ||
![]() |
138c19126b | ||
![]() |
c459ceba73 | ||
![]() |
8d0ceff652 | ||
![]() |
1d383e80a4 | ||
![]() |
6a17a12be5 | ||
![]() |
3a8d962d34 | ||
![]() |
7e5cf17cf4 | ||
![]() |
214940d04f | ||
![]() |
6877fdaf5b | ||
![]() |
35d0c254a2 | ||
![]() |
9649fbc189 | ||
![]() |
b60b1fc0c6 | ||
![]() |
6b93f6d75c | ||
![]() |
c8069a383e | ||
![]() |
6857e87b30 | ||
![]() |
a095631f4f | ||
![]() |
c59fbdeec1 | ||
![]() |
b521b1e64c | ||
![]() |
073589ae19 | ||
![]() |
9435b0ad3a | ||
![]() |
1662d36125 | ||
![]() |
70e54fdadd | ||
![]() |
38d0ebb8ba | ||
![]() |
15cb48badb | ||
![]() |
22214e8d31 | ||
![]() |
fc04e0b2cc | ||
![]() |
3fc6ebdb43 | ||
![]() |
3ccb7deb3c | ||
![]() |
f5f63b914a | ||
![]() |
bd0a3f5a5d | ||
![]() |
ab9eebd092 | ||
![]() |
68c43099d9 | ||
![]() |
041c417164 | ||
![]() |
537d09c697 | ||
![]() |
21e3b8da92 | ||
![]() |
d390681360 | ||
![]() |
918ec78348 | ||
![]() |
1deae3ee1a | ||
![]() |
59eace67df | ||
![]() |
7eb7c66e3f | ||
![]() |
aa2941592d | ||
![]() |
29daf136d2 | ||
![]() |
3da3cf7f52 | ||
![]() |
d8c93d54d5 | ||
![]() |
0799ee9fba | ||
![]() |
9d31403984 | ||
![]() |
02f87cba9b | ||
![]() |
5b54784378 |
@@ -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")
|
||||
|
@@ -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,
|
||||
|
33
homeassistant/components/airos/diagnostics.py
Normal file
33
homeassistant/components/airos/diagnostics.py
Normal 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),
|
||||
}
|
@@ -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"]
|
||||
}
|
||||
|
@@ -41,7 +41,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: done
|
||||
|
@@ -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",
|
||||
|
@@ -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"
|
||||
},
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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"
|
||||
]
|
||||
}
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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"
|
||||
]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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"
|
||||
],
|
||||
|
@@ -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()
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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": {
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
),
|
||||
|
@@ -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,
|
||||
),
|
||||
|
@@ -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
|
||||
),
|
||||
|
@@ -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
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2.1.1"]
|
||||
"requirements": ["aioautomower==2.1.2"]
|
||||
}
|
||||
|
@@ -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 = [
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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)}},
|
||||
)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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]
|
||||
|
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pylitterbot==2024.2.2"]
|
||||
"requirements": ["pylitterbot==2024.2.3"]
|
||||
}
|
||||
|
@@ -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.5–100.0%
|
||||
mode=NumberMode.SLIDER,
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiomealie==0.10.0"]
|
||||
"requirements": ["aiomealie==0.10.1"]
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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),
|
||||
)
|
||||
|
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
@@ -203,7 +203,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse:
|
||||
else {}
|
||||
),
|
||||
}
|
||||
if item["parameters"]
|
||||
if item.get("parameters")
|
||||
else {}
|
||||
),
|
||||
}
|
||||
|
@@ -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)",
|
||||
|
@@ -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)
|
||||
|
@@ -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):
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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%]",
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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:
|
||||
|
@@ -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%]",
|
||||
|
@@ -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)
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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),
|
||||
)
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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(
|
||||
|
@@ -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.
|
||||
|
@@ -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(
|
||||
|
@@ -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 haven’t 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 select’s current value.",
|
||||
"select_option": "Defines actions to run when an `option` from the `options` list is selected. Receives variable `option`.",
|
||||
"options": "Template for the select’s 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%]",
|
||||
|
@@ -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."""
|
||||
|
@@ -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,
|
||||
|
@@ -28,7 +28,7 @@ class TeslemetryData:
|
||||
vehicles: list[TeslemetryVehicleData]
|
||||
energysites: list[TeslemetryEnergyData]
|
||||
scopes: list[Scope]
|
||||
stream: TeslemetryStream
|
||||
stream: TeslemetryStream | None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@@ -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."""
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
)
|
||||
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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,
|
||||
)
|
||||
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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."""
|
||||
|
@@ -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"
|
||||
|
@@ -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."]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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:
|
||||
|
@@ -21,7 +21,7 @@
|
||||
"zha",
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": ["zha==0.0.62"],
|
||||
"requirements": ["zha==0.0.65"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10C4",
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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:
|
||||
|
@@ -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()
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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."""
|
||||
|
@@ -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",
|
||||
|
@@ -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.",
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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()
|
||||
|
@@ -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
Reference in New Issue
Block a user