Compare commits

...

120 Commits

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -81,11 +81,15 @@ async def async_update_options(
async def async_migrate_integration(hass: HomeAssistant) -> None:
"""Migrate integration entry structure."""
entries = hass.config_entries.async_entries(DOMAIN)
# Make sure we get enabled config entries first
entries = sorted(
hass.config_entries.async_entries(DOMAIN),
key=lambda e: e.disabled_by is not None,
)
if not any(entry.version == 1 for entry in entries):
return
api_keys_entries: dict[str, ConfigEntry] = {}
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
@@ -99,30 +103,61 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
)
if entry.data[CONF_API_KEY] not in api_keys_entries:
use_existing = True
api_keys_entries[entry.data[CONF_API_KEY]] = entry
all_disabled = all(
e.disabled_by is not None
for e in entries
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
)
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
hass.config_entries.async_add_subentry(parent_entry, subentry)
conversation_entity = entity_registry.async_get_entity_id(
conversation_entity_id = entity_registry.async_get_entity_id(
"conversation",
DOMAIN,
entry.entry_id,
)
if conversation_entity is not None:
entity_registry.async_update_entity(
conversation_entity,
config_entry_id=parent_entry.entry_id,
config_subentry_id=subentry.subentry_id,
new_unique_id=subentry.subentry_id,
)
device = device_registry.async_get_device(
identifiers={(DOMAIN, entry.entry_id)}
)
if conversation_entity_id is not None:
conversation_entity_entry = entity_registry.entities[conversation_entity_id]
entity_disabled_by = conversation_entity_entry.disabled_by
if (
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
and not all_disabled
):
# Device and entity registries don't update the disabled_by flag
# when moving a device or entity from one config entry to another,
# so we need to do it manually.
entity_disabled_by = (
er.RegistryEntryDisabler.DEVICE
if device
else er.RegistryEntryDisabler.USER
)
entity_registry.async_update_entity(
conversation_entity_id,
config_entry_id=parent_entry.entry_id,
config_subentry_id=subentry.subentry_id,
disabled_by=entity_disabled_by,
new_unique_id=subentry.subentry_id,
)
if device is not None:
# Device and entity registries don't update the disabled_by flag when
# moving a device or entity from one config entry to another, so we
# need to do it manually.
device_disabled_by = device.disabled_by
if (
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
and not all_disabled
):
device_disabled_by = dr.DeviceEntryDisabler.USER
device_registry.async_update_device(
device.id,
disabled_by=device_disabled_by,
new_identifiers={(DOMAIN, subentry.subentry_id)},
add_config_subentry_id=subentry.subentry_id,
add_config_entry_id=parent_entry.entry_id,
@@ -147,7 +182,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
title=DEFAULT_CONVERSATION_NAME,
options={},
version=2,
minor_version=2,
minor_version=3,
)
@@ -173,6 +208,38 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
hass.config_entries.async_update_entry(entry, minor_version=2)
if entry.version == 2 and entry.minor_version == 2:
# Fix migration where the disabled_by flag was not set correctly.
# We can currently only correct this for enabled config entries,
# because migration does not run for disabled config entries. This
# is asserted in tests, and if that behavior is changed, we should
# correct also disabled config entries.
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
entity_entries = er.async_entries_for_config_entry(
entity_registry, entry.entry_id
)
if entry.disabled_by is None:
# If the config entry is not disabled, we need to set the disabled_by
# flag on devices to USER, and on entities to DEVICE, if they are set
# to CONFIG_ENTRY.
for device in devices:
if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY:
continue
device_registry.async_update_device(
device.id,
disabled_by=dr.DeviceEntryDisabler.USER,
)
for entity in entity_entries:
if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY:
continue
entity_registry.async_update_entity(
entity.entity_id,
disabled_by=er.RegistryEntryDisabler.DEVICE,
)
hass.config_entries.async_update_entry(entry, minor_version=3)
LOGGER.debug(
"Migration to version %s:%s successful", entry.version, entry.minor_version
)

View File

@@ -75,7 +75,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Anthropic."""
VERSION = 2
MINOR_VERSION = 2
MINOR_VERSION = 3
async def async_step_user(
self, user_input: dict[str, Any] | None = None

View File

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

View File

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

View File

@@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==64"],
"requirements": ["axis==65"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# If path is relative, we assume relative to Home Assistant config dir
if not os.path.isabs(download_path):
download_path = hass.config.path(download_path)
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_DOWNLOAD_DIR: download_path}
)
if not await hass.async_add_executor_job(os.path.isdir, download_path):
_LOGGER.error(

View File

@@ -11,6 +11,7 @@ import requests
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
@@ -34,24 +35,33 @@ def download_file(service: ServiceCall) -> None:
entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0]
download_path = entry.data[CONF_DOWNLOAD_DIR]
url: str = service.data[ATTR_URL]
subdir: str | None = service.data.get(ATTR_SUBDIR)
target_filename: str | None = service.data.get(ATTR_FILENAME)
overwrite: bool = service.data[ATTR_OVERWRITE]
if subdir:
# Check the path
try:
raise_if_invalid_path(subdir)
except ValueError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="subdir_invalid",
translation_placeholders={"subdir": subdir},
) from err
if os.path.isabs(subdir):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="subdir_not_relative",
translation_placeholders={"subdir": subdir},
)
def do_download() -> None:
"""Download the file."""
final_path = None
filename = target_filename
try:
url = service.data[ATTR_URL]
subdir = service.data.get(ATTR_SUBDIR)
filename = service.data.get(ATTR_FILENAME)
overwrite = service.data.get(ATTR_OVERWRITE)
if subdir:
# Check the path
raise_if_invalid_path(subdir)
final_path = None
req = requests.get(url, stream=True, timeout=10)
if req.status_code != HTTPStatus.OK:

View File

@@ -12,6 +12,14 @@
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"exceptions": {
"subdir_invalid": {
"message": "Invalid subdirectory, got: {subdir}"
},
"subdir_not_relative": {
"message": "Subdirectory must be relative, got: {subdir}"
}
},
"services": {
"download_file": {
"name": "Download file",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -123,10 +123,10 @@
},
"ai_task_data": {
"initiate_flow": {
"user": "Add Generate data with AI service",
"reconfigure": "Reconfigure Generate data with AI service"
"user": "Add AI task",
"reconfigure": "Reconfigure AI task"
},
"entry_type": "Generate data with AI service",
"entry_type": "AI task",
"step": {
"set_options": {
"data": {

View File

@@ -86,9 +86,11 @@ UNSUPPORTED_REASONS = {
UNSUPPORTED_SKIP_REPAIR = {"privileged"}
UNHEALTHY_REASONS = {
"docker",
"supervisor",
"setup",
"duplicate_os_installation",
"oserror_bad_message",
"privileged",
"setup",
"supervisor",
"untrusted",
}

View File

@@ -116,35 +116,43 @@
},
"unhealthy": {
"title": "Unhealthy system - {reason}",
"description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this."
"description": "System is currently unhealthy due to {reason}. For troubleshooting information, select Learn more."
},
"unhealthy_docker": {
"title": "Unhealthy system - Docker misconfigured",
"description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this."
"description": "System is currently unhealthy because Docker is configured incorrectly. For troubleshooting information, select Learn more."
},
"unhealthy_supervisor": {
"title": "Unhealthy system - Supervisor update failed",
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this."
"unhealthy_duplicate_os_installation": {
"description": "System is currently unhealthy because it has detected multiple Home Assistant OS installations. For troubleshooting information, select Learn more.",
"title": "Unhealthy system - Duplicate Home Assistant OS installation"
},
"unhealthy_setup": {
"title": "Unhealthy system - Setup failed",
"description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this."
"unhealthy_oserror_bad_message": {
"description": "System is currently unhealthy because the operating system has reported an OS error: Bad message. For troubleshooting information, select Learn more.",
"title": "Unhealthy system - Operating System error: Bad message"
},
"unhealthy_privileged": {
"title": "Unhealthy system - Not privileged",
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this."
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. For troubleshooting information, select Learn more."
},
"unhealthy_setup": {
"title": "Unhealthy system - Setup failed",
"description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, For troubleshooting information, select Learn more."
},
"unhealthy_supervisor": {
"title": "Unhealthy system - Supervisor update failed",
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. For troubleshooting information, select Learn more."
},
"unhealthy_untrusted": {
"title": "Unhealthy system - Untrusted code",
"description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this."
"description": "System is currently unhealthy because it has detected untrusted code or images in use. For troubleshooting information, select Learn more."
},
"unsupported": {
"title": "Unsupported system - {reason}",
"description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this."
"description": "System is unsupported due to {reason}. For troubleshooting information, select Learn more."
},
"unsupported_apparmor": {
"title": "Unsupported system - AppArmor issues",
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this."
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. For troubleshooting information, select Learn more."
},
"unsupported_cgroup_version": {
"title": "Unsupported system - CGroup version",
@@ -152,23 +160,23 @@
},
"unsupported_connectivity_check": {
"title": "Unsupported system - Connectivity check disabled",
"description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. Use the link to learn more and how to fix this."
"description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. For troubleshooting information, select Learn more."
},
"unsupported_content_trust": {
"title": "Unsupported system - Content-trust check disabled",
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this."
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. For troubleshooting information, select Learn more."
},
"unsupported_dbus": {
"title": "Unsupported system - D-Bus issues",
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this."
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. For troubleshooting information, select Learn more."
},
"unsupported_dns_server": {
"title": "Unsupported system - DNS server issues",
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this."
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. For troubleshooting information, select Learn more."
},
"unsupported_docker_configuration": {
"title": "Unsupported system - Docker misconfigured",
"description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this."
"description": "System is unsupported because the Docker daemon is running in an unexpected way. For troubleshooting information, select Learn more."
},
"unsupported_docker_version": {
"title": "Unsupported system - Docker version",
@@ -176,15 +184,15 @@
},
"unsupported_job_conditions": {
"title": "Unsupported system - Protections disabled",
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this."
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. For troubleshooting information, select Learn more."
},
"unsupported_lxc": {
"title": "Unsupported system - LXC detected",
"description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this."
"description": "System is unsupported because it is being run in an LXC virtual machine. For troubleshooting information, select Learn more."
},
"unsupported_network_manager": {
"title": "Unsupported system - Network Manager issues",
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
},
"unsupported_os": {
"title": "Unsupported system - Operating System",
@@ -192,39 +200,43 @@
},
"unsupported_os_agent": {
"title": "Unsupported system - OS-Agent issues",
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
},
"unsupported_restart_policy": {
"title": "Unsupported system - Container restart policy",
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this."
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. For troubleshooting information, select Learn more."
},
"unsupported_software": {
"title": "Unsupported system - Unsupported software",
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this."
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. For troubleshooting information, select Learn more."
},
"unsupported_source_mods": {
"title": "Unsupported system - Supervisor source modifications",
"description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this."
"description": "System is unsupported because Supervisor source code has been modified. For troubleshooting information, select Learn more."
},
"unsupported_supervisor_version": {
"title": "Unsupported system - Supervisor version",
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this."
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. For troubleshooting information, select Learn more."
},
"unsupported_systemd": {
"title": "Unsupported system - Systemd issues",
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
},
"unsupported_systemd_journal": {
"title": "Unsupported system - Systemd Journal issues",
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
},
"unsupported_systemd_resolved": {
"title": "Unsupported system - Systemd-Resolved issues",
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
},
"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."
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. For troubleshooting information, select Learn more."
},
"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. For troubleshooting information, select Learn more."
}
},
"entity": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -140,11 +140,6 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
def _update_from_device(self) -> None:
"""Update from device."""
self._calculate_features()
# optional battery level
if VacuumEntityFeature.BATTERY & self._attr_supported_features:
self._attr_battery_level = self.get_matter_attribute_value(
clusters.PowerSource.Attributes.BatPercentRemaining
)
# derive state from the run mode + operational state
run_mode_raw: int = self.get_matter_attribute_value(
clusters.RvcRunMode.Attributes.CurrentMode
@@ -188,11 +183,6 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
supported_features |= VacuumEntityFeature.STATE
supported_features |= VacuumEntityFeature.STOP
# optional battery attribute = battery feature
if self.get_matter_attribute_value(
clusters.PowerSource.Attributes.BatPercentRemaining
):
supported_features |= VacuumEntityFeature.BATTERY
# optional identify cluster = locate feature (value must be not None or 0)
if self.get_matter_attribute_value(clusters.Identify.Attributes.IdentifyType):
supported_features |= VacuumEntityFeature.LOCATE
@@ -230,7 +220,6 @@ DISCOVERY_SCHEMAS = [
clusters.RvcRunMode.Attributes.CurrentMode,
clusters.RvcOperationalState.Attributes.OperationalState,
),
optional_attributes=(clusters.PowerSource.Attributes.BatPercentRemaining,),
device_type=(device_types.RoboticVacuumCleaner,),
allow_none_value=True,
),

View File

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

View File

@@ -130,6 +130,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
list_id=self._shopping_list_id,
note=item.summary.strip() if item.summary else item.summary,
position=position,
quantity=0.0,
)
try:
await self.coordinator.client.add_shopping_item(new_shopping_item)
@@ -174,7 +175,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 +251,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
mutate_shopping_item.note = item.note
mutate_shopping_item.checked = item.checked
if item.is_food:
if item.is_food or item.food_id:
mutate_shopping_item.food_id = item.food_id
mutate_shopping_item.unit_id = item.unit_id

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,9 @@
{
"issues": {
"deprecated_vacuum_battery_feature": {
"title": "Deprecated battery feature used",
"description": "Vacuum entity {entity_id} implements the battery feature which is deprecated. This will stop working in Home Assistant 2026.2. Implement a separate entity for the battery state instead. To fix the issue, remove the `battery` feature from the configured supported features, and restart Home Assistant."
},
"invalid_platform_config": {
"title": "Invalid config found for MQTT {domain} item",
"description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue."
@@ -426,7 +430,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 +806,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 +842,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 +1108,7 @@
},
"device_class_sensor": {
"options": {
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
"area": "[%key:component::sensor::entity_component::area::name%]",
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",

View File

@@ -17,7 +17,7 @@ from homeassistant.components.vacuum import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolSchemaType
@@ -25,11 +25,11 @@ from homeassistant.util.json import json_loads_object
from . import subscription
from .config import MQTT_BASE_SCHEMA
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC
from .entity import MqttEntity, async_setup_entity_entry_helper
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN
from .entity import IssueSeverity, MqttEntity, async_setup_entity_entry_helper
from .models import ReceiveMessage
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
from .util import valid_publish_topic
from .util import learn_more_url, valid_publish_topic
PARALLEL_UPDATES = 0
@@ -84,6 +84,8 @@ SERVICE_TO_STRING: dict[VacuumEntityFeature, str] = {
VacuumEntityFeature.STOP: "stop",
VacuumEntityFeature.RETURN_HOME: "return_home",
VacuumEntityFeature.FAN_SPEED: "fan_speed",
# Use of the battery feature was deprecated in HA Core 2025.8
# and will be removed with HA Core 2026.2
VacuumEntityFeature.BATTERY: "battery",
VacuumEntityFeature.STATUS: "status",
VacuumEntityFeature.SEND_COMMAND: "send_command",
@@ -96,7 +98,6 @@ DEFAULT_SERVICES = (
VacuumEntityFeature.START
| VacuumEntityFeature.STOP
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
)
ALL_SERVICES = (
@@ -251,10 +252,35 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
)
}
async def mqtt_async_added_to_hass(self) -> None:
"""Check for use of deprecated battery features."""
if self.supported_features & VacuumEntityFeature.BATTERY:
ir.async_create_issue(
self.hass,
DOMAIN,
f"deprecated_vacuum_battery_feature_{self.entity_id}",
issue_domain=vacuum.DOMAIN,
breaks_in_ha_version="2026.2",
is_fixable=False,
severity=IssueSeverity.WARNING,
learn_more_url=learn_more_url(vacuum.DOMAIN),
translation_placeholders={"entity_id": self.entity_id},
translation_key="deprecated_vacuum_battery_feature",
)
_LOGGER.warning(
"MQTT vacuum entity %s implements the battery feature "
"which is deprecated. This will stop working "
"in Home Assistant 2026.2. Implement a separate entity "
"for the battery status instead",
self.entity_id,
)
def _update_state_attributes(self, payload: dict[str, Any]) -> None:
"""Update the entity state attributes."""
self._state_attrs.update(payload)
self._attr_fan_speed = self._state_attrs.get(FAN_SPEED, 0)
# Use of the battery feature was deprecated in HA Core 2025.8
# and will be removed with HA Core 2026.2
self._attr_battery_level = max(0, min(100, self._state_attrs.get(BATTERY, 0)))
@callback

View File

@@ -58,10 +58,10 @@
},
"ai_task_data": {
"initiate_flow": {
"user": "Add Generate data with AI service",
"reconfigure": "Reconfigure Generate data with AI service"
"user": "Add AI task",
"reconfigure": "Reconfigure AI task"
},
"entry_type": "Generate data with AI service",
"entry_type": "AI task",
"step": {
"set_options": {
"data": {

View File

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

View File

@@ -52,9 +52,9 @@
}
},
"initiate_flow": {
"user": "Add Generate data with AI service"
"user": "Add AI task"
},
"entry_type": "Generate data with AI service",
"entry_type": "AI task",
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"

View File

@@ -272,11 +272,15 @@ async def async_update_options(hass: HomeAssistant, entry: OpenAIConfigEntry) ->
async def async_migrate_integration(hass: HomeAssistant) -> None:
"""Migrate integration entry structure."""
entries = hass.config_entries.async_entries(DOMAIN)
# Make sure we get enabled config entries first
entries = sorted(
hass.config_entries.async_entries(DOMAIN),
key=lambda e: e.disabled_by is not None,
)
if not any(entry.version == 1 for entry in entries):
return
api_keys_entries: dict[str, ConfigEntry] = {}
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
@@ -290,30 +294,61 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
)
if entry.data[CONF_API_KEY] not in api_keys_entries:
use_existing = True
api_keys_entries[entry.data[CONF_API_KEY]] = entry
all_disabled = all(
e.disabled_by is not None
for e in entries
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
)
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
hass.config_entries.async_add_subentry(parent_entry, subentry)
conversation_entity = entity_registry.async_get_entity_id(
conversation_entity_id = entity_registry.async_get_entity_id(
"conversation",
DOMAIN,
entry.entry_id,
)
if conversation_entity is not None:
entity_registry.async_update_entity(
conversation_entity,
config_entry_id=parent_entry.entry_id,
config_subentry_id=subentry.subentry_id,
new_unique_id=subentry.subentry_id,
)
device = device_registry.async_get_device(
identifiers={(DOMAIN, entry.entry_id)}
)
if conversation_entity_id is not None:
conversation_entity_entry = entity_registry.entities[conversation_entity_id]
entity_disabled_by = conversation_entity_entry.disabled_by
if (
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
and not all_disabled
):
# Device and entity registries don't update the disabled_by flag
# when moving a device or entity from one config entry to another,
# so we need to do it manually.
entity_disabled_by = (
er.RegistryEntryDisabler.DEVICE
if device
else er.RegistryEntryDisabler.USER
)
entity_registry.async_update_entity(
conversation_entity_id,
config_entry_id=parent_entry.entry_id,
config_subentry_id=subentry.subentry_id,
disabled_by=entity_disabled_by,
new_unique_id=subentry.subentry_id,
)
if device is not None:
# Device and entity registries don't update the disabled_by flag when
# moving a device or entity from one config entry to another, so we
# need to do it manually.
device_disabled_by = device.disabled_by
if (
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
and not all_disabled
):
device_disabled_by = dr.DeviceEntryDisabler.USER
device_registry.async_update_device(
device.id,
disabled_by=device_disabled_by,
new_identifiers={(DOMAIN, subentry.subentry_id)},
add_config_subentry_id=subentry.subentry_id,
add_config_entry_id=parent_entry.entry_id,
@@ -333,12 +368,13 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
if not use_existing:
await hass.config_entries.async_remove(entry.entry_id)
else:
_add_ai_task_subentry(hass, entry)
hass.config_entries.async_update_entry(
entry,
title=DEFAULT_NAME,
options={},
version=2,
minor_version=2,
minor_version=4,
)
@@ -365,19 +401,56 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) ->
hass.config_entries.async_update_entry(entry, minor_version=2)
if entry.version == 2 and entry.minor_version == 2:
hass.config_entries.async_add_subentry(
entry,
ConfigSubentry(
data=MappingProxyType(RECOMMENDED_AI_TASK_OPTIONS),
subentry_type="ai_task_data",
title=DEFAULT_AI_TASK_NAME,
unique_id=None,
),
)
_add_ai_task_subentry(hass, entry)
hass.config_entries.async_update_entry(entry, minor_version=3)
if entry.version == 2 and entry.minor_version == 3:
# Fix migration where the disabled_by flag was not set correctly.
# We can currently only correct this for enabled config entries,
# because migration does not run for disabled config entries. This
# is asserted in tests, and if that behavior is changed, we should
# correct also disabled config entries.
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
entity_entries = er.async_entries_for_config_entry(
entity_registry, entry.entry_id
)
if entry.disabled_by is None:
# If the config entry is not disabled, we need to set the disabled_by
# flag on devices to USER, and on entities to DEVICE, if they are set
# to CONFIG_ENTRY.
for device in devices:
if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY:
continue
device_registry.async_update_device(
device.id,
disabled_by=dr.DeviceEntryDisabler.USER,
)
for entity in entity_entries:
if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY:
continue
entity_registry.async_update_entity(
entity.entity_id,
disabled_by=er.RegistryEntryDisabler.DEVICE,
)
hass.config_entries.async_update_entry(entry, minor_version=4)
LOGGER.debug(
"Migration to version %s:%s successful", entry.version, entry.minor_version
)
return True
def _add_ai_task_subentry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> None:
"""Add AI Task subentry to the config entry."""
hass.config_entries.async_add_subentry(
entry,
ConfigSubentry(
data=MappingProxyType(RECOMMENDED_AI_TASK_OPTIONS),
subentry_type="ai_task_data",
title=DEFAULT_AI_TASK_NAME,
unique_id=None,
),
)

View File

@@ -98,7 +98,7 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for OpenAI Conversation."""
VERSION = 2
MINOR_VERSION = 3
MINOR_VERSION = 4
async def async_step_user(
self, user_input: dict[str, Any] | None = None

View File

@@ -73,10 +73,10 @@
},
"ai_task_data": {
"initiate_flow": {
"user": "Add Generate data with AI service",
"reconfigure": "Reconfigure Generate data with AI service"
"user": "Add AI task",
"reconfigure": "Reconfigure AI task"
},
"entry_type": "Generate data with AI service",
"entry_type": "AI task",
"step": {
"init": {
"data": {

View File

@@ -9,6 +9,8 @@ from typing import Any
from opower import (
CannotConnect,
InvalidAuth,
MfaChallenge,
MfaHandlerBase,
Opower,
create_cookie_jar,
get_supported_utility_names,
@@ -16,49 +18,34 @@ from opower import (
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.typing import VolDictType
from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN
from .const import CONF_LOGIN_DATA, CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names()),
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
CONF_MFA_CODE = "mfa_code"
CONF_MFA_METHOD = "mfa_method"
async def _validate_login(
hass: HomeAssistant, login_data: dict[str, str]
) -> dict[str, str]:
"""Validate login data and return any errors."""
hass: HomeAssistant,
data: Mapping[str, Any],
) -> None:
"""Validate login data and raise exceptions on failure."""
api = Opower(
async_create_clientsession(hass, cookie_jar=create_cookie_jar()),
login_data[CONF_UTILITY],
login_data[CONF_USERNAME],
login_data[CONF_PASSWORD],
login_data.get(CONF_TOTP_SECRET),
data[CONF_UTILITY],
data[CONF_USERNAME],
data[CONF_PASSWORD],
data.get(CONF_TOTP_SECRET),
data.get(CONF_LOGIN_DATA),
)
errors: dict[str, str] = {}
try:
await api.async_login()
except InvalidAuth:
_LOGGER.exception(
"Invalid auth when connecting to %s", login_data[CONF_UTILITY]
)
errors["base"] = "invalid_auth"
except CannotConnect:
_LOGGER.exception("Could not connect to %s", login_data[CONF_UTILITY])
errors["base"] = "cannot_connect"
return errors
await api.async_login()
class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -68,81 +55,147 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize a new OpowerConfigFlow."""
self.utility_info: dict[str, Any] | None = None
self._data: dict[str, Any] = {}
self.mfa_handler: MfaHandlerBase | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
"""Handle the initial step (select utility)."""
if user_input is not None:
self._async_abort_entries_match(
{
CONF_UTILITY: user_input[CONF_UTILITY],
CONF_USERNAME: user_input[CONF_USERNAME],
}
)
if select_utility(user_input[CONF_UTILITY]).accepts_mfa():
self.utility_info = user_input
return await self.async_step_mfa()
self._data[CONF_UTILITY] = user_input[CONF_UTILITY]
return await self.async_step_credentials()
errors = await _validate_login(self.hass, user_input)
if not errors:
return self._async_create_opower_entry(user_input)
else:
user_input = {}
user_input.pop(CONF_PASSWORD, None)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names())}
),
)
async def async_step_credentials(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle credentials step."""
errors: dict[str, str] = {}
utility = select_utility(self._data[CONF_UTILITY])
if user_input is not None:
self._data.update(user_input)
self._async_abort_entries_match(
{
CONF_UTILITY: self._data[CONF_UTILITY],
CONF_USERNAME: self._data[CONF_USERNAME],
}
)
try:
await _validate_login(self.hass, self._data)
except MfaChallenge as exc:
self.mfa_handler = exc.handler
return await self.async_step_mfa_options()
except InvalidAuth:
errors["base"] = "invalid_auth"
except CannotConnect:
errors["base"] = "cannot_connect"
else:
return self._async_create_opower_entry(self._data)
schema_dict: VolDictType = {
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
if utility.accepts_totp_secret():
schema_dict[vol.Optional(CONF_TOTP_SECRET)] = str
return self.async_show_form(
step_id="credentials",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, user_input
vol.Schema(schema_dict), user_input
),
errors=errors,
)
async def async_step_mfa(
async def async_step_mfa_options(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle MFA step."""
assert self.utility_info is not None
"""Handle MFA options step."""
errors: dict[str, str] = {}
assert self.mfa_handler is not None
if user_input is not None:
method = user_input[CONF_MFA_METHOD]
try:
await self.mfa_handler.async_select_mfa_option(method)
except CannotConnect:
errors["base"] = "cannot_connect"
else:
return await self.async_step_mfa_code()
mfa_options = await self.mfa_handler.async_get_mfa_options()
if not mfa_options:
return await self.async_step_mfa_code()
return self.async_show_form(
step_id="mfa_options",
data_schema=self.add_suggested_values_to_schema(
vol.Schema({vol.Required(CONF_MFA_METHOD): vol.In(mfa_options)}),
user_input,
),
errors=errors,
)
async def async_step_mfa_code(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle MFA code submission step."""
assert self.mfa_handler is not None
errors: dict[str, str] = {}
if user_input is not None:
data = {**self.utility_info, **user_input}
errors = await _validate_login(self.hass, data)
if not errors:
return self._async_create_opower_entry(data)
if errors:
schema = {
vol.Required(
CONF_USERNAME, default=self.utility_info[CONF_USERNAME]
): str,
vol.Required(CONF_PASSWORD): str,
}
else:
schema = {}
schema[vol.Required(CONF_TOTP_SECRET)] = str
code = user_input[CONF_MFA_CODE]
try:
login_data = await self.mfa_handler.async_submit_mfa_code(code)
except InvalidAuth:
errors["base"] = "invalid_mfa_code"
except CannotConnect:
errors["base"] = "cannot_connect"
else:
self._data[CONF_LOGIN_DATA] = login_data
if self.source == SOURCE_REAUTH:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=self._data
)
return self._async_create_opower_entry(self._data)
return self.async_show_form(
step_id="mfa",
data_schema=vol.Schema(schema),
step_id="mfa_code",
data_schema=self.add_suggested_values_to_schema(
vol.Schema({vol.Required(CONF_MFA_CODE): str}), user_input
),
errors=errors,
)
@callback
def _async_create_opower_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
def _async_create_opower_entry(
self, data: dict[str, Any], **kwargs: Any
) -> ConfigFlowResult:
"""Create the config entry."""
return self.async_create_entry(
title=f"{data[CONF_UTILITY]} ({data[CONF_USERNAME]})",
data=data,
**kwargs,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
return await self.async_step_reauth_confirm()
reauth_entry = self._get_reauth_entry()
self._data = dict(reauth_entry.data)
return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={CONF_NAME: reauth_entry.title},
)
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
@@ -150,21 +203,34 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
data = {**reauth_entry.data, **user_input}
errors = await _validate_login(self.hass, data)
if not errors:
return self.async_update_reload_and_abort(reauth_entry, data=data)
schema: VolDictType = {
vol.Required(CONF_USERNAME): reauth_entry.data[CONF_USERNAME],
if user_input is not None:
self._data.update(user_input)
try:
await _validate_login(self.hass, self._data)
except MfaChallenge as exc:
self.mfa_handler = exc.handler
return await self.async_step_mfa_options()
except InvalidAuth:
errors["base"] = "invalid_auth"
except CannotConnect:
errors["base"] = "cannot_connect"
else:
return self.async_update_reload_and_abort(reauth_entry, data=self._data)
utility = select_utility(self._data[CONF_UTILITY])
schema_dict: VolDictType = {
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
if select_utility(reauth_entry.data[CONF_UTILITY]).accepts_mfa():
schema[vol.Optional(CONF_TOTP_SECRET)] = str
if utility.accepts_totp_secret():
schema_dict[vol.Optional(CONF_TOTP_SECRET)] = str
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(schema),
data_schema=self.add_suggested_values_to_schema(
vol.Schema(schema_dict), self._data
),
errors=errors,
description_placeholders={CONF_NAME: reauth_entry.title},
)

View File

@@ -4,3 +4,4 @@ DOMAIN = "opower"
CONF_UTILITY = "utility"
CONF_TOTP_SECRET = "totp_secret"
CONF_LOGIN_DATA = "login_data"

View File

@@ -14,7 +14,7 @@ from opower import (
ReadResolution,
create_cookie_jar,
)
from opower.exceptions import ApiException, CannotConnect, InvalidAuth
from opower.exceptions import ApiException, CannotConnect, InvalidAuth, MfaChallenge
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import (
@@ -36,7 +36,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN
from .const import CONF_LOGIN_DATA, CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -69,6 +69,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
config_entry.data[CONF_USERNAME],
config_entry.data[CONF_PASSWORD],
config_entry.data.get(CONF_TOTP_SECRET),
config_entry.data.get(CONF_LOGIN_DATA),
)
@callback
@@ -90,7 +91,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
# Given the infrequent updating (every 12h)
# assume previous session has expired and re-login.
await self.api.async_login()
except InvalidAuth as err:
except (InvalidAuth, MfaChallenge) as err:
_LOGGER.error("Error during login: %s", err)
raise ConfigEntryAuthFailed from err
except CannotConnect as err:

View File

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

View File

@@ -3,27 +3,43 @@
"step": {
"user": {
"data": {
"utility": "Utility name",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
"utility": "Utility name"
},
"data_description": {
"utility": "The name of your utility provider",
"username": "The username for your utility account",
"password": "The password for your utility account"
"utility": "The name of your utility provider"
}
},
"mfa": {
"description": "The TOTP secret below is not one of the 6-digit time-based numeric codes. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation.",
"credentials": {
"title": "Enter Credentials",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"totp_secret": "TOTP secret"
},
"data_description": {
"username": "[%key:component::opower::config::step::user::data_description::username%]",
"password": "[%key:component::opower::config::step::user::data_description::password%]",
"totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)."
"username": "The username for your utility account",
"password": "The password for your utility account",
"totp_secret": "This is not a 6-digit code. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation."
}
},
"mfa_options": {
"title": "Multi-factor authentication",
"description": "Your account requires multi-factor authentication (MFA). Select a method to receive your security code.",
"data": {
"mfa_method": "MFA method"
},
"data_description": {
"mfa_method": "How to receive your security code"
}
},
"mfa_code": {
"title": "Enter security code",
"description": "A security code has been sent via your selected method. Please enter it below to complete login.",
"data": {
"mfa_code": "Security code"
},
"data_description": {
"mfa_code": "Typically a 6-digit code"
}
},
"reauth_confirm": {
@@ -31,18 +47,19 @@
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"totp_secret": "[%key:component::opower::config::step::mfa::data::totp_secret%]"
"totp_secret": "[%key:component::opower::config::step::credentials::data::totp_secret%]"
},
"data_description": {
"username": "[%key:component::opower::config::step::user::data_description::username%]",
"password": "[%key:component::opower::config::step::user::data_description::password%]",
"totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)."
"username": "[%key:component::opower::config::step::credentials::data_description::username%]",
"password": "[%key:component::opower::config::step::credentials::data_description::password%]",
"totp_secret": "[%key:component::opower::config::step::credentials::data_description::totp_secret%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_mfa_code": "The security code is incorrect. Please try again."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ PLATFORMS = [
Platform.UPDATE,
]
DEVICE_UPDATE_INTERVAL = timedelta(seconds=60)
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=12)
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=24)
NUM_CRED_ERRORS = 3
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)

View File

@@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics(
IPC_cam[ch]["hardware version"] = api.camera_hardware_version(ch)
IPC_cam[ch]["firmware version"] = api.camera_sw_version(ch)
IPC_cam[ch]["encoding main"] = await api.get_encoding(ch)
if (signal := api.wifi_signal(ch)) is not None:
if (signal := api.wifi_signal(ch)) is not None and api.wifi_connection(ch):
IPC_cam[ch]["WiFi signal"] = signal
chimes: dict[int, dict[str, Any]] = {}
@@ -43,7 +43,7 @@ async def async_get_config_entry_diagnostics(
"HTTP(S) port": api.port,
"Baichuan port": api.baichuan.port,
"Baichuan only": api.baichuan_only,
"WiFi connection": api.wifi_connection,
"WiFi connection": api.wifi_connection(),
"WiFi signal": api.wifi_signal(),
"RTMP enabled": api.rtmp_enabled,
"RTSP enabled": api.rtsp_enabled,

View File

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

View File

@@ -148,7 +148,7 @@ HOST_SENSORS = (
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
entity_registry_enabled_default=False,
value=lambda api: api.wifi_signal(),
supported=lambda api: api.supported(None, "wifi") and api.wifi_connection,
supported=lambda api: api.supported(None, "wifi") and api.wifi_connection(),
),
ReolinkHostSensorEntityDescription(
key="cpu_usage",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
"documentation": "https://www.home-assistant.io/integrations/sonos",
"iot_class": "local_push",
"loggers": ["soco", "sonos_websocket"],
"requirements": ["soco==0.30.9", "sonos-websocket==0.1.3"],
"requirements": ["soco==0.30.11", "sonos-websocket==0.1.3"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"

View File

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

View File

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

View File

@@ -41,5 +41,5 @@
"iot_class": "local_push",
"loggers": ["switchbot"],
"quality_scale": "gold",
"requirements": ["PySwitchbot==0.68.2"]
"requirements": ["PySwitchbot==0.68.3"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -153,6 +153,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool
# Register known device IDs
device_registry = dr.async_get(hass)
for device in manager.device_map.values():
LOGGER.debug(
"Register device %s: %s (function: %s, status range: %s)",
device.id,
device.status,
device.function,
device.status_range,
)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, device.id)},
@@ -237,6 +244,14 @@ class DeviceListener(SharingDeviceListener):
# Ensure the device isn't present stale
self.hass.add_job(self.async_remove_device, device.id)
LOGGER.debug(
"Add device %s: %s (function: %s, status range: %s)",
device.id,
device.status,
device.function,
device.status_range,
)
dispatcher_send(self.hass, TUYA_DISCOVERY_NEW, [device.id])
def remove_device(self, device_id: str) -> None:

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