mirror of
https://github.com/home-assistant/core.git
synced 2026-01-18 21:46:54 +01:00
Compare commits
237 Commits
llm-python
...
2025.8.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b1b095b47 | ||
|
|
bb4f8adffe | ||
|
|
61a50e77cf | ||
|
|
82f94de0b8 | ||
|
|
71b2d46afd | ||
|
|
edc1989ff6 | ||
|
|
2dad6fa298 | ||
|
|
2f4e29ba71 | ||
|
|
add75e06e3 | ||
|
|
9194ddd4fe | ||
|
|
2e7821d64a | ||
|
|
bb9660269c | ||
|
|
1bd5aa0ab0 | ||
|
|
e4329ab8a5 | ||
|
|
9414356a4d | ||
|
|
0cd28e7fc1 | ||
|
|
6383f9365c | ||
|
|
cb8669c84f | ||
|
|
d1698222f4 | ||
|
|
a3f5c3f422 | ||
|
|
945771098e | ||
|
|
59d73138e7 | ||
|
|
fe71b54c3e | ||
|
|
7639e12ff2 | ||
|
|
4b2a149072 | ||
|
|
92b988a292 | ||
|
|
1ca6c4b5b8 | ||
|
|
81377be92f | ||
|
|
38aba81f62 | ||
|
|
27b32c5e93 | ||
|
|
332996cc38 | ||
|
|
199b7e8ba7 | ||
|
|
122af46a92 | ||
|
|
3dd091de44 | ||
|
|
932c5ccf0f | ||
|
|
4e52826664 | ||
|
|
c30d778a54 | ||
|
|
d7320f00ea | ||
|
|
0bcc0f3fb9 | ||
|
|
c551a133c1 | ||
|
|
22e19e768e | ||
|
|
0647222402 | ||
|
|
83226ed015 | ||
|
|
837472c12d | ||
|
|
0b337c7e2a | ||
|
|
1a0b61c98e | ||
|
|
87a2d3e6d9 | ||
|
|
2c1407f159 | ||
|
|
1643d5df67 | ||
|
|
5a49007b86 | ||
|
|
312d8aaff5 | ||
|
|
776726a053 | ||
|
|
4213427b9c | ||
|
|
82907e5b88 | ||
|
|
56b4c554de | ||
|
|
82390f6f7b | ||
|
|
d9ebda4910 | ||
|
|
8f94657b0c | ||
|
|
b0ab3cddb8 | ||
|
|
3d4d57fa32 | ||
|
|
fed6f19edf | ||
|
|
2725abf032 | ||
|
|
bd1b81493c | ||
|
|
c58a188179 | ||
|
|
ffbb7a2ab4 | ||
|
|
2ad470d172 | ||
|
|
8b2fce9c33 | ||
|
|
e22e7f1bcf | ||
|
|
b5bd61b20a | ||
|
|
391c9a679e | ||
|
|
8d49cb1195 | ||
|
|
dc8aaac6fb | ||
|
|
1e87f0cab1 | ||
|
|
1aeced0fe6 | ||
|
|
7ed14f0afd | ||
|
|
dc5d159ffb | ||
|
|
5fdd04b860 | ||
|
|
6f5d72fd81 | ||
|
|
3158aa8891 | ||
|
|
0c74e22069 | ||
|
|
a88549315c | ||
|
|
fde548b825 | ||
|
|
91b10fb6d7 | ||
|
|
39f41fe17d | ||
|
|
3d39fb08e5 | ||
|
|
a1731cd210 | ||
|
|
0c31ec9bb6 | ||
|
|
762c179b80 | ||
|
|
66019953db | ||
|
|
a2931efeeb | ||
|
|
90c03f4115 | ||
|
|
8afe3fed74 | ||
|
|
3ef332e168 | ||
|
|
8d821d9f98 | ||
|
|
23619fb2d3 | ||
|
|
bc70aeea85 | ||
|
|
beca01e857 | ||
|
|
4765d9da92 | ||
|
|
7951e822be | ||
|
|
c653bfff9f | ||
|
|
2223bdb48e | ||
|
|
42a3bef34a | ||
|
|
6f4d405b26 | ||
|
|
8edc5f0359 | ||
|
|
efcffd1016 | ||
|
|
ee32992010 | ||
|
|
319128043e | ||
|
|
f074d81c8b | ||
|
|
d791d66104 | ||
|
|
dd9bd50a7b | ||
|
|
6243517271 | ||
|
|
ad8ff7570d | ||
|
|
c4c14bee36 | ||
|
|
2cf5badc17 | ||
|
|
0478f43b4b | ||
|
|
d18f6273a8 | ||
|
|
94bade0202 | ||
|
|
855e8b08e9 | ||
|
|
9820956b46 | ||
|
|
1693299652 | ||
|
|
75200a9426 | ||
|
|
fa587cec38 | ||
|
|
47946d0103 | ||
|
|
d2586ca4ff | ||
|
|
4e21ef5fbc | ||
|
|
a9998b41a5 | ||
|
|
0a72f31504 | ||
|
|
f3a50c176d | ||
|
|
b6b422775a | ||
|
|
00baecd01e | ||
|
|
b370b7a7f6 | ||
|
|
baa2d751e4 | ||
|
|
c8d54fcffc | ||
|
|
80e3655bac | ||
|
|
e5b0a366fe | ||
|
|
20e78a15b4 | ||
|
|
9d806aef88 | ||
|
|
7e16973166 | ||
|
|
e5f776fdc3 | ||
|
|
83ccdb35f1 | ||
|
|
52984f2fd1 | ||
|
|
a548e13da5 | ||
|
|
55301a50b2 | ||
|
|
808273962d | ||
|
|
094fe43557 | ||
|
|
8f5bd51eef | ||
|
|
faf0ded854 | ||
|
|
d20302f97b | ||
|
|
74c25496bc | ||
|
|
67ecea0778 | ||
|
|
164e5871cb | ||
|
|
7a9966120e | ||
|
|
d810b4ca38 | ||
|
|
896062d669 | ||
|
|
03bd133577 | ||
|
|
4596c1644b | ||
|
|
778fe96eb6 | ||
|
|
a06557ed54 | ||
|
|
641621d184 | ||
|
|
b163f2b855 | ||
|
|
0c0604e5bd | ||
|
|
e0e4fc8afb | ||
|
|
f832a2844f | ||
|
|
4b0b268227 | ||
|
|
dfc16d9f15 | ||
|
|
4e3309bd22 | ||
|
|
a5a45ce59f | ||
|
|
6cb48da2f3 | ||
|
|
ab5aac47b2 | ||
|
|
d50b9405f0 | ||
|
|
a2722f08c4 | ||
|
|
aa700c3982 | ||
|
|
3b1bb41129 | ||
|
|
79ef51fb07 | ||
|
|
53769da55e | ||
|
|
82d153a240 | ||
|
|
0dac635478 | ||
|
|
90fc7d314b | ||
|
|
636c1b7e4f | ||
|
|
49c23de2d2 | ||
|
|
e48820b2c1 | ||
|
|
2b7a434677 | ||
|
|
9ef7c6c99a | ||
|
|
b789c11217 | ||
|
|
5e8cd19cc3 | ||
|
|
027052440d | ||
|
|
47a7ed4084 | ||
|
|
89f6cfeb81 | ||
|
|
c268e57ba7 | ||
|
|
138c19126b | ||
|
|
c459ceba73 | ||
|
|
8d0ceff652 | ||
|
|
1d383e80a4 | ||
|
|
6a17a12be5 | ||
|
|
3a8d962d34 | ||
|
|
7e5cf17cf4 | ||
|
|
214940d04f | ||
|
|
6877fdaf5b | ||
|
|
35d0c254a2 | ||
|
|
9649fbc189 | ||
|
|
b60b1fc0c6 | ||
|
|
6b93f6d75c | ||
|
|
c8069a383e | ||
|
|
6857e87b30 | ||
|
|
a095631f4f | ||
|
|
c59fbdeec1 | ||
|
|
b521b1e64c | ||
|
|
073589ae19 | ||
|
|
9435b0ad3a | ||
|
|
1662d36125 | ||
|
|
70e54fdadd | ||
|
|
38d0ebb8ba | ||
|
|
15cb48badb | ||
|
|
22214e8d31 | ||
|
|
fc04e0b2cc | ||
|
|
3fc6ebdb43 | ||
|
|
3ccb7deb3c | ||
|
|
f5f63b914a | ||
|
|
bd0a3f5a5d | ||
|
|
ab9eebd092 | ||
|
|
68c43099d9 | ||
|
|
041c417164 | ||
|
|
537d09c697 | ||
|
|
21e3b8da92 | ||
|
|
d390681360 | ||
|
|
918ec78348 | ||
|
|
1deae3ee1a | ||
|
|
59eace67df | ||
|
|
7eb7c66e3f | ||
|
|
aa2941592d | ||
|
|
29daf136d2 | ||
|
|
3da3cf7f52 | ||
|
|
d8c93d54d5 | ||
|
|
0799ee9fba | ||
|
|
9d31403984 | ||
|
|
02f87cba9b | ||
|
|
5b54784378 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 4
|
||||
CACHE_VERSION: 6
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.8"
|
||||
|
||||
2
Dockerfile
generated
2
Dockerfile
generated
@@ -31,7 +31,7 @@ RUN \
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.7.1
|
||||
RUN pip3 install uv==0.8.9
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from airos.exceptions import (
|
||||
ConnectionAuthenticationError,
|
||||
ConnectionSetupError,
|
||||
DataMissingError,
|
||||
DeviceConnectionError,
|
||||
KeyDataMissingError,
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
AirOSKeyDataMissingError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -59,13 +59,13 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
airos_data = await airos_device.status()
|
||||
|
||||
except (
|
||||
ConnectionSetupError,
|
||||
DeviceConnectionError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
):
|
||||
errors["base"] = "cannot_connect"
|
||||
except (ConnectionAuthenticationError, DataMissingError):
|
||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||
errors["base"] = "invalid_auth"
|
||||
except KeyDataMissingError:
|
||||
except AirOSKeyDataMissingError:
|
||||
errors["base"] = "key_data_missing"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
|
||||
@@ -6,10 +6,10 @@ import logging
|
||||
|
||||
from airos.airos8 import AirOS, AirOSData
|
||||
from airos.exceptions import (
|
||||
ConnectionAuthenticationError,
|
||||
ConnectionSetupError,
|
||||
DataMissingError,
|
||||
DeviceConnectionError,
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -47,18 +47,22 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
||||
try:
|
||||
await self.airos_device.login()
|
||||
return await self.airos_device.status()
|
||||
except (ConnectionAuthenticationError,) as err:
|
||||
except (AirOSConnectionAuthenticationError,) as err:
|
||||
_LOGGER.exception("Error authenticating with airOS device")
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from err
|
||||
except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err:
|
||||
except (
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
TimeoutError,
|
||||
) as err:
|
||||
_LOGGER.error("Error connecting to airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
except (DataMissingError,) as err:
|
||||
except (AirOSDataMissingError,) as err:
|
||||
_LOGGER.error("Expected data not returned by airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
33
homeassistant/components/airos/diagnostics.py
Normal file
33
homeassistant/components/airos/diagnostics.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Diagnostics support for airOS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AirOSConfigEntry
|
||||
|
||||
IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related
|
||||
HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address
|
||||
TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD]
|
||||
TO_REDACT_AIROS = [
|
||||
"hostname", # Prevent leaking device naming
|
||||
"essid", # Network SSID
|
||||
"lat", # GPS latitude to prevent exposing location data.
|
||||
"lon", # GPS longitude to prevent exposing location data.
|
||||
*HW_REDACT,
|
||||
*IP_REDACT,
|
||||
]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AirOSConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {
|
||||
"entry_data": async_redact_data(entry.data, TO_REDACT_HA),
|
||||
"data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS),
|
||||
}
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["airos==0.2.1"]
|
||||
"requirements": ["airos==0.2.11"]
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: done
|
||||
|
||||
@@ -69,13 +69,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
translation_key="wireless_essid",
|
||||
value_fn=lambda data: data.wireless.essid,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_mode",
|
||||
translation_key="wireless_mode",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(),
|
||||
options=WIRELESS_MODE_OPTIONS,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_antenna_gain",
|
||||
translation_key="wireless_antenna_gain",
|
||||
|
||||
@@ -43,13 +43,6 @@
|
||||
"wireless_essid": {
|
||||
"name": "Wireless SSID"
|
||||
},
|
||||
"wireless_mode": {
|
||||
"name": "Wireless mode",
|
||||
"state": {
|
||||
"ap_ptp": "Access point",
|
||||
"sta_ptp": "Station"
|
||||
}
|
||||
},
|
||||
"wireless_antenna_gain": {
|
||||
"name": "Antenna gain"
|
||||
},
|
||||
|
||||
@@ -7,21 +7,18 @@ import logging
|
||||
|
||||
from airthings import Airthings
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_SECRET
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
|
||||
"""Set up Airthings from a config entry."""
|
||||
@@ -31,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -13,15 +14,23 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
|
||||
"""Coordinator for Airthings data updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
airthings: Airthings,
|
||||
config_entry: AirthingsConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_method=self._update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Alexa Devices integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import CONF_COUNTRY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import _LOGGER, COUNTRY_DOMAINS, DOMAIN
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
@@ -40,6 +40,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
if entry.version == 1 and entry.minor_version == 0:
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s.%s", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
# Convert country in domain
|
||||
country = entry.data[CONF_COUNTRY]
|
||||
domain = COUNTRY_DOMAINS.get(country, country)
|
||||
|
||||
# Save domain and remove country
|
||||
new_data = entry.data.copy()
|
||||
new_data.update({"site": f"https://www.amazon.{domain}"})
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data=new_data, version=1, minor_version=1
|
||||
)
|
||||
|
||||
_LOGGER.info(
|
||||
"Migration to version %s.%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -10,16 +10,14 @@ from aioamazondevices.exceptions import (
|
||||
CannotAuthenticate,
|
||||
CannotConnect,
|
||||
CannotRetrieveData,
|
||||
WrongCountry,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.selector import CountrySelector
|
||||
|
||||
from .const import CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
@@ -37,7 +35,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
session = aiohttp_client.async_create_clientsession(hass)
|
||||
api = AmazonEchoApi(
|
||||
session,
|
||||
data[CONF_COUNTRY],
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
)
|
||||
@@ -48,6 +45,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Alexa Devices."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -62,8 +62,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "invalid_auth"
|
||||
except CannotRetrieveData:
|
||||
errors["base"] = "cannot_retrieve_data"
|
||||
except WrongCountry:
|
||||
errors["base"] = "wrong_country"
|
||||
else:
|
||||
await self.async_set_unique_id(data["customer_info"]["user_id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
@@ -78,9 +76,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_COUNTRY, default=self.hass.config.country
|
||||
): CountrySelector(),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
|
||||
@@ -6,3 +6,22 @@ _LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "alexa_devices"
|
||||
CONF_LOGIN_DATA = "login_data"
|
||||
|
||||
DEFAULT_DOMAIN = {"domain": "com"}
|
||||
COUNTRY_DOMAINS = {
|
||||
"ar": DEFAULT_DOMAIN,
|
||||
"at": DEFAULT_DOMAIN,
|
||||
"au": {"domain": "com.au"},
|
||||
"be": {"domain": "com.be"},
|
||||
"br": DEFAULT_DOMAIN,
|
||||
"gb": {"domain": "co.uk"},
|
||||
"il": DEFAULT_DOMAIN,
|
||||
"jp": {"domain": "co.jp"},
|
||||
"mx": {"domain": "com.mx"},
|
||||
"no": DEFAULT_DOMAIN,
|
||||
"nz": {"domain": "com.au"},
|
||||
"pl": DEFAULT_DOMAIN,
|
||||
"tr": {"domain": "com.tr"},
|
||||
"us": DEFAULT_DOMAIN,
|
||||
"za": {"domain": "co.za"},
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ from aioamazondevices.exceptions import (
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@@ -44,7 +44,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
)
|
||||
self.api = AmazonEchoApi(
|
||||
session,
|
||||
entry.data[CONF_COUNTRY],
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_LOGIN_DATA],
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioamazondevices==4.0.0"]
|
||||
"requirements": ["aioamazondevices==5.0.0"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"common": {
|
||||
"data_code": "One-time password (OTP code)",
|
||||
"data_description_country": "The country where your Amazon account is registered.",
|
||||
"data_description_username": "The email address of your Amazon account.",
|
||||
"data_description_password": "The password of your Amazon account.",
|
||||
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.",
|
||||
@@ -12,13 +11,11 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "[%key:common::config_flow::data::country%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"country": "[%key:component::alexa_devices::common::data_description_country%]",
|
||||
"username": "[%key:component::alexa_devices::common::data_description_username%]",
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
@@ -46,7 +43,6 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN, REQUEST_TIMEOUT
|
||||
|
||||
API_URL = "https://app.amber.com.au/developers"
|
||||
|
||||
@@ -64,7 +64,9 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
api = amberelectric.AmberApi(api_client)
|
||||
|
||||
try:
|
||||
sites: list[Site] = filter_sites(api.get_sites())
|
||||
sites: list[Site] = filter_sites(
|
||||
api.get_sites(_request_timeout=REQUEST_TIMEOUT)
|
||||
)
|
||||
except amberelectric.ApiException as api_exception:
|
||||
if api_exception.status == 403:
|
||||
self._errors[CONF_API_TOKEN] = "invalid_api_token"
|
||||
|
||||
@@ -22,3 +22,5 @@ SERVICE_GET_FORECASTS = "get_forecasts"
|
||||
GENERAL_CHANNEL = "general"
|
||||
CONTROLLED_LOAD_CHANNEL = "controlled_load"
|
||||
FEED_IN_CHANNEL = "feed_in"
|
||||
|
||||
REQUEST_TIMEOUT = 15
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import LOGGER
|
||||
from .const import LOGGER, REQUEST_TIMEOUT
|
||||
from .helpers import normalize_descriptor
|
||||
|
||||
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
|
||||
@@ -82,7 +82,11 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
|
||||
"grid": {},
|
||||
}
|
||||
try:
|
||||
data = self._api.get_current_prices(self.site_id, next=288)
|
||||
data = self._api.get_current_prices(
|
||||
self.site_id,
|
||||
next=288,
|
||||
_request_timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
intervals = [interval.actual_instance for interval in data]
|
||||
except ApiException as api_exception:
|
||||
raise UpdateFailed("Missing price data, skipping update") from api_exception
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyasuswrt import AsusWrtError
|
||||
|
||||
@@ -40,6 +40,9 @@ from .const import (
|
||||
SENSORS_CONNECTED_DEVICE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AsusWrtConfigEntry
|
||||
|
||||
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
@@ -52,10 +55,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AsusWrtSensorDataHandler:
|
||||
"""Data handler for AsusWrt sensor."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, api: AsusWrtBridge, entry: AsusWrtConfigEntry
|
||||
) -> None:
|
||||
"""Initialize a AsusWrt sensor data handler."""
|
||||
self._hass = hass
|
||||
self._api = api
|
||||
self._entry = entry
|
||||
self._connected_devices = 0
|
||||
|
||||
async def _get_connected_devices(self) -> dict[str, int]:
|
||||
@@ -91,6 +97,7 @@ class AsusWrtSensorDataHandler:
|
||||
update_method=method,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=SCAN_INTERVAL if should_poll else None,
|
||||
config_entry=self._entry,
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
@@ -321,7 +328,9 @@ class AsusWrtRouter:
|
||||
if self._sensors_data_handler:
|
||||
return
|
||||
|
||||
self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api)
|
||||
self._sensors_data_handler = AsusWrtSensorDataHandler(
|
||||
self.hass, self._api, self._entry
|
||||
)
|
||||
self._sensors_data_handler.update_device_count(self._connected_devices)
|
||||
|
||||
sensors_types = await self._api.async_get_available_sensors()
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"]
|
||||
"requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"]
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==64"],
|
||||
"requirements": ["axis==65"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==1.0.1",
|
||||
"bleak-retry-connector==4.0.0",
|
||||
"bleak-retry-connector==4.0.2",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -69,12 +69,7 @@ class SHCEntity(SHCBaseEntity):
|
||||
manufacturer=device.manufacturer,
|
||||
model=device.device_model,
|
||||
name=device.name,
|
||||
via_device=(
|
||||
DOMAIN,
|
||||
device.parent_device_id
|
||||
if device.parent_device_id is not None
|
||||
else parent_id,
|
||||
),
|
||||
via_device=(DOMAIN, device.root_device_id),
|
||||
)
|
||||
super().__init__(device=device, parent_id=parent_id, entry_id=entry_id)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bosch_shc",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["boschshcpy"],
|
||||
"requirements": ["boschshcpy==0.2.91"],
|
||||
"requirements": ["boschshcpy==0.2.107"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"services": {
|
||||
"press": {
|
||||
"name": "Press",
|
||||
"description": "Press the button entity."
|
||||
"description": "Presses a button entity."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.3.1"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.110.0"],
|
||||
"requirements": ["hass-nabucasa==0.111.2"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.23"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.7.30"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["cookidoo_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["cookidoo-api==0.12.2"]
|
||||
"requirements": ["cookidoo-api==0.14.0"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.2.0",
|
||||
"aiodiscover==2.7.0",
|
||||
"aiodhcpwatcher==1.2.1",
|
||||
"aiodiscover==2.7.1",
|
||||
"cached-ipaddress==0.10.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Data update coordinator for the Enigma2 integration."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from openwebif.api import OpenWebIfDevice, OpenWebIfStatus
|
||||
@@ -30,6 +31,8 @@ from .const import CONF_SOURCE_BOUQUET, DOMAIN
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
SETUP_TIMEOUT = 10
|
||||
|
||||
type Enigma2ConfigEntry = ConfigEntry[Enigma2UpdateCoordinator]
|
||||
|
||||
|
||||
@@ -79,7 +82,7 @@ class Enigma2UpdateCoordinator(DataUpdateCoordinator[OpenWebIfStatus]):
|
||||
async def _async_setup(self) -> None:
|
||||
"""Provide needed data to the device info."""
|
||||
|
||||
about = await self.device.get_about()
|
||||
about = await asyncio.wait_for(self.device.get_about(), timeout=SETUP_TIMEOUT)
|
||||
self.device.mac_address = about["info"]["ifaces"][0]["mac"]
|
||||
self.device_info["model"] = about["info"]["model"]
|
||||
self.device_info["manufacturer"] = about["info"]["brand"]
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pyenphase import Envoy
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
@@ -42,6 +44,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b
|
||||
},
|
||||
)
|
||||
|
||||
# register envoy before via_device is used
|
||||
device_registry = dr.async_get(hass)
|
||||
if TYPE_CHECKING:
|
||||
assert envoy.serial_number
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, envoy.serial_number)},
|
||||
manufacturer="Enphase",
|
||||
name=coordinator.name,
|
||||
model=envoy.envoy_model,
|
||||
sw_version=str(envoy.firmware),
|
||||
hw_version=envoy.part_number,
|
||||
serial_number=envoy.serial_number,
|
||||
)
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -116,6 +116,8 @@ async def async_get_config_entry_diagnostics(
|
||||
entities.append({"entity": entity_dict, "state": state_dict})
|
||||
device_dict = asdict(device)
|
||||
device_dict.pop("_cache", None)
|
||||
# This can be removed when suggested_area is removed from DeviceEntry
|
||||
device_dict.pop("_suggested_area")
|
||||
device_entities.append({"device": device_dict, "entities": entities})
|
||||
|
||||
# remove envoy serial
|
||||
|
||||
@@ -316,10 +316,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# Don't call _fetch_device_info() for ignored entries
|
||||
raise AbortFlow("already_configured")
|
||||
configured_host: str | None = entry.data.get(CONF_HOST)
|
||||
configured_port: int | None = entry.data.get(CONF_PORT)
|
||||
if configured_host == host and configured_port == port:
|
||||
configured_port: int = entry.data.get(CONF_PORT, DEFAULT_PORT)
|
||||
# When port is None (from DHCP discovery), only compare hosts
|
||||
if configured_host == host and (port is None or configured_port == port):
|
||||
# Don't probe to verify the mac is correct since
|
||||
# the host and port matches.
|
||||
# the host matches (and port matches if provided).
|
||||
raise AbortFlow("already_configured")
|
||||
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
|
||||
await self._fetch_device_info(host, port or configured_port, configured_psk)
|
||||
|
||||
@@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
|
||||
|
||||
DEFAULT_PORT: Final = 6053
|
||||
|
||||
STABLE_BLE_VERSION_STR = "2025.5.0"
|
||||
STABLE_BLE_VERSION_STR = "2025.8.0"
|
||||
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
|
||||
PROJECT_URLS = {
|
||||
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==37.1.5",
|
||||
"aioesphomeapi==37.2.2",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.1.0"
|
||||
],
|
||||
|
||||
@@ -106,6 +106,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_logger_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
await self.logger_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -120,6 +121,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_meters_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -129,6 +131,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_ohmpilot_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -138,6 +141,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_power_flow_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -147,6 +151,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_storages_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -206,6 +211,7 @@ class FroniusSolarNet:
|
||||
logger=_LOGGER,
|
||||
name=_inverter_name,
|
||||
inverter_info=_inverter_info,
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
await _coordinator.async_refresh()
|
||||
|
||||
@@ -50,7 +50,7 @@ CONF_EXTRA_JS_URL_ES5 = "extra_js_url_es5"
|
||||
CONF_FRONTEND_REPO = "development_repo"
|
||||
CONF_JS_VERSION = "javascript_version"
|
||||
|
||||
DEFAULT_THEME_COLOR = "#03A9F4"
|
||||
DEFAULT_THEME_COLOR = "#2980b9"
|
||||
|
||||
|
||||
DATA_PANELS: HassKey[dict[str, Panel]] = HassKey("frontend_panels")
|
||||
|
||||
@@ -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==20250811.1"]
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["habiticalib"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["habiticalib==0.4.1"]
|
||||
"requirements": ["habiticalib==0.4.2"]
|
||||
}
|
||||
|
||||
@@ -61,18 +61,19 @@ PLACEHOLDER_KEY_REASON = "reason"
|
||||
|
||||
UNSUPPORTED_REASONS = {
|
||||
"apparmor",
|
||||
"cgroup_version",
|
||||
"connectivity_check",
|
||||
"content_trust",
|
||||
"dbus",
|
||||
"dns_server",
|
||||
"docker_configuration",
|
||||
"docker_version",
|
||||
"cgroup_version",
|
||||
"job_conditions",
|
||||
"lxc",
|
||||
"network_manager",
|
||||
"os",
|
||||
"os_agent",
|
||||
"os_version",
|
||||
"restart_policy",
|
||||
"software",
|
||||
"source_mods",
|
||||
@@ -80,15 +81,18 @@ UNSUPPORTED_REASONS = {
|
||||
"systemd",
|
||||
"systemd_journal",
|
||||
"systemd_resolved",
|
||||
"virtualization_image",
|
||||
}
|
||||
# Some unsupported reasons also mark the system as unhealthy. If the unsupported reason
|
||||
# provides no additional information beyond the unhealthy one then skip that repair.
|
||||
UNSUPPORTED_SKIP_REPAIR = {"privileged"}
|
||||
UNHEALTHY_REASONS = {
|
||||
"docker",
|
||||
"supervisor",
|
||||
"setup",
|
||||
"duplicate_os_installation",
|
||||
"oserror_bad_message",
|
||||
"privileged",
|
||||
"setup",
|
||||
"supervisor",
|
||||
"untrusted",
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.77", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.79", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ from ha_silabs_firmware_client import (
|
||||
ManifestMissing,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -24,13 +25,20 @@ FIRMWARE_REFRESH_INTERVAL = timedelta(hours=8)
|
||||
class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]):
|
||||
"""Coordinator to manage firmware updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, session: ClientSession, url: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
session: ClientSession,
|
||||
url: str,
|
||||
) -> None:
|
||||
"""Initialize the firmware update coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="firmware update coordinator",
|
||||
update_interval=FIRMWARE_REFRESH_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
self.hass = hass
|
||||
self.session = session
|
||||
|
||||
@@ -124,6 +124,7 @@ def _async_create_update_entity(
|
||||
config_entry=config_entry,
|
||||
update_coordinator=FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
config_entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
),
|
||||
|
||||
@@ -129,6 +129,7 @@ def _async_create_update_entity(
|
||||
config_entry=config_entry,
|
||||
update_coordinator=FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
config_entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
),
|
||||
|
||||
@@ -163,6 +163,7 @@ async def async_setup_entry(
|
||||
name="light",
|
||||
update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update),
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
request_refresh_debouncer=Debouncer(
|
||||
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||
),
|
||||
@@ -197,6 +198,7 @@ async def async_setup_entry(
|
||||
name="group",
|
||||
update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update),
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
request_refresh_debouncer=Debouncer(
|
||||
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||
),
|
||||
|
||||
@@ -53,6 +53,7 @@ class SensorManager:
|
||||
LOGGER,
|
||||
name="sensor",
|
||||
update_method=self.async_update_data,
|
||||
config_entry=bridge.config_entry,
|
||||
update_interval=self.SCAN_INTERVAL,
|
||||
request_refresh_debouncer=debounce.Debouncer(
|
||||
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||
|
||||
@@ -12,6 +12,7 @@ from aioautomower.exceptions import (
|
||||
ApiError,
|
||||
AuthError,
|
||||
HusqvarnaTimeoutError,
|
||||
HusqvarnaWSClientError,
|
||||
HusqvarnaWSServerHandshakeError,
|
||||
)
|
||||
from aioautomower.model import MowerDictionary
|
||||
@@ -142,7 +143,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
# Reset reconnect time after successful connection
|
||||
self.reconnect_time = DEFAULT_RECONNECT_TIME
|
||||
await automower_client.start_listening()
|
||||
except HusqvarnaWSServerHandshakeError as err:
|
||||
except (HusqvarnaWSServerHandshakeError, HusqvarnaWSClientError) as err:
|
||||
_LOGGER.debug(
|
||||
"Failed to connect to websocket. Trying to reconnect: %s",
|
||||
err,
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2.1.1"]
|
||||
"requirements": ["aioautomower==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -71,10 +71,10 @@ ERROR_KEYS = [
|
||||
"cutting_drive_motor_2_defect",
|
||||
"cutting_drive_motor_3_defect",
|
||||
"cutting_height_blocked",
|
||||
"cutting_height_problem",
|
||||
"cutting_height_problem_curr",
|
||||
"cutting_height_problem_dir",
|
||||
"cutting_height_problem_drive",
|
||||
"cutting_height_problem",
|
||||
"cutting_motor_problem",
|
||||
"cutting_stopped_slope_too_steep",
|
||||
"cutting_system_blocked",
|
||||
@@ -117,7 +117,6 @@ ERROR_KEYS = [
|
||||
"no_accurate_position_from_satellites",
|
||||
"no_confirmed_position",
|
||||
"no_drive",
|
||||
"no_error",
|
||||
"no_loop_signal",
|
||||
"no_power_in_charging_station",
|
||||
"no_response_from_charger",
|
||||
@@ -169,8 +168,8 @@ ERROR_KEYS = [
|
||||
]
|
||||
|
||||
|
||||
ERROR_KEY_LIST = list(
|
||||
dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES])
|
||||
ERROR_KEY_LIST = sorted(
|
||||
set(ERROR_KEYS) | {state.lower() for state in ERROR_STATES} | {"no_error"}
|
||||
)
|
||||
|
||||
INACTIVE_REASONS: list = [
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .account import IcloudAccount
|
||||
from .account import IcloudAccount, IcloudConfigEntry
|
||||
from .const import (
|
||||
ATTR_ACCOUNT,
|
||||
ATTR_DEVICE_NAME,
|
||||
@@ -92,8 +92,10 @@ def lost_device(service: ServiceCall) -> None:
|
||||
def update_account(service: ServiceCall) -> None:
|
||||
"""Call the update function of an iCloud account."""
|
||||
if (account := service.data.get(ATTR_ACCOUNT)) is None:
|
||||
for account in service.hass.data[DOMAIN].values():
|
||||
account.keep_alive()
|
||||
# Update all accounts when no specific account is provided
|
||||
entry: IcloudConfigEntry
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
entry.runtime_data.keep_alive()
|
||||
else:
|
||||
_get_account(service.hass, account).keep_alive()
|
||||
|
||||
@@ -102,17 +104,12 @@ def _get_account(hass: HomeAssistant, account_identifier: str) -> IcloudAccount:
|
||||
if account_identifier is None:
|
||||
return None
|
||||
|
||||
icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier)
|
||||
if icloud_account is None:
|
||||
for account in hass.data[DOMAIN].values():
|
||||
if account.username == account_identifier:
|
||||
icloud_account = account
|
||||
entry: IcloudConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if entry.runtime_data.username == account_identifier:
|
||||
return entry.runtime_data
|
||||
|
||||
if icloud_account is None:
|
||||
raise ValueError(
|
||||
f"No iCloud account with username or name {account_identifier}"
|
||||
)
|
||||
return icloud_account
|
||||
raise ValueError(f"No iCloud account with username or name {account_identifier}")
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -75,13 +75,11 @@ class InverterCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]):
|
||||
data: dict[str, str | float | int] = {}
|
||||
|
||||
async with timeout(TIMEOUT):
|
||||
await self._api.login(
|
||||
self.config_entry.data[CONF_USERNAME],
|
||||
self.config_entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
# Fetch data using distant API
|
||||
try:
|
||||
await self._api.login(
|
||||
self.config_entry.data[CONF_USERNAME],
|
||||
self.config_entry.data[CONF_PASSWORD],
|
||||
)
|
||||
await self._api.update()
|
||||
except (ValueError, ClientError) as e:
|
||||
raise UpdateFailed(e) from e
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.const import (
|
||||
EntityCategory,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfFrequency,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
@@ -50,8 +49,8 @@ SENSOR_DESCRIPTIONS = (
|
||||
SensorEntityDescription(
|
||||
key="battery_stored",
|
||||
translation_key="battery_stored",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Grid
|
||||
@@ -238,16 +237,16 @@ SENSOR_DESCRIPTIONS = (
|
||||
SensorEntityDescription(
|
||||
key="pv_consumed",
|
||||
translation_key="pv_consumed",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="pv_injected",
|
||||
translation_key="pv_injected",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="pv_power_1",
|
||||
@@ -290,14 +289,14 @@ SENSOR_DESCRIPTIONS = (
|
||||
key="monitoring_self_consumption",
|
||||
translation_key="monitoring_self_consumption",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="monitoring_self_sufficiency",
|
||||
translation_key="monitoring_self_sufficiency",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
# Monitoring (instant minute data)
|
||||
|
||||
@@ -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.4"]
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
),
|
||||
}
|
||||
)
|
||||
self.add_suggested_values_to_schema(
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
data_schema,
|
||||
{"section_1": {"int": self.config_entry.options.get(CONF_INT, 10)}},
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.8.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.7.23.50952"
|
||||
"knx-frontend==2025.8.9.63154"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ class KrakenData:
|
||||
self._hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
config_entry=self._config_entry,
|
||||
update_method=self.async_update,
|
||||
update_interval=timedelta(
|
||||
seconds=self._config_entry.options[CONF_SCAN_INTERVAL]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Do you want to configure the Launch Library?"
|
||||
"description": "Do you want to configure Launch Library?"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
name=f"{DOMAIN}_{ha_bridge.device.device_id}",
|
||||
)
|
||||
|
||||
self.data = {}
|
||||
self.data = ha_bridge.update_status(None)
|
||||
self.api = ha_bridge
|
||||
self.device_id = ha_bridge.device.device_id
|
||||
self.sub_id = ha_bridge.sub_id
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pylitterbot==2024.2.2"]
|
||||
"requirements": ["pylitterbot==2024.2.3"]
|
||||
}
|
||||
|
||||
@@ -285,7 +285,9 @@ DISCOVERY_SCHEMAS = [
|
||||
native_min_value=0.5,
|
||||
native_step=0.5,
|
||||
device_to_ha=(
|
||||
lambda x: None if x is None else x / 2 # Matter range (1-200)
|
||||
lambda x: None
|
||||
if x is None
|
||||
else min(x, 200) / 2 # Matter range (1-200, capped at 200)
|
||||
),
|
||||
ha_to_device=lambda x: round(x * 2), # HA range 0.5–100.0%
|
||||
mode=NumberMode.SLIDER,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -52,9 +52,12 @@ class MatterValve(MatterEntity, ValveEntity):
|
||||
|
||||
async def async_set_valve_position(self, position: int) -> None:
|
||||
"""Move the valve to a specific position."""
|
||||
await self.send_device_command(
|
||||
ValveConfigurationAndControl.Commands.Open(targetLevel=position)
|
||||
)
|
||||
if position > 0:
|
||||
await self.send_device_command(
|
||||
ValveConfigurationAndControl.Commands.Open(targetLevel=position)
|
||||
)
|
||||
return
|
||||
await self.send_device_command(ValveConfigurationAndControl.Commands.Close())
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiomealie==0.10.0"]
|
||||
"requirements": ["aiomealie==0.10.1"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.08.11"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"Météo-France forecast for city {entry.title}",
|
||||
config_entry=entry,
|
||||
update_method=_async_update_data_forecast_forecast,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
@@ -80,6 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"Météo-France rain for city {entry.title}",
|
||||
config_entry=entry,
|
||||
update_method=_async_update_data_rain,
|
||||
update_interval=SCAN_INTERVAL_RAIN,
|
||||
)
|
||||
@@ -103,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"Météo-France alert for department {department}",
|
||||
config_entry=entry,
|
||||
update_method=_async_update_data_alert,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> boo
|
||||
) from err
|
||||
|
||||
# Setup MieleAPI and coordinator for data fetch
|
||||
coordinator = MieleDataUpdateCoordinator(hass, auth)
|
||||
coordinator = MieleDataUpdateCoordinator(hass, entry, auth)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
|
||||
@@ -431,6 +431,16 @@ DISHWASHER_PROGRAM_ID: dict[int, str] = {
|
||||
38: "quick_power_wash",
|
||||
42: "tall_items",
|
||||
44: "power_wash",
|
||||
200: "eco",
|
||||
202: "automatic",
|
||||
203: "comfort_wash",
|
||||
204: "power_wash",
|
||||
205: "intensive",
|
||||
207: "extra_quiet",
|
||||
209: "comfort_wash_plus",
|
||||
210: "gentle",
|
||||
214: "maintenance",
|
||||
215: "rinse_salt",
|
||||
}
|
||||
TUMBLE_DRYER_PROGRAM_ID: dict[int, str] = {
|
||||
-1: "no_program", # Extrapolated from other device types.
|
||||
@@ -1320,4 +1330,5 @@ class PlatePowerStep(MieleEnum):
|
||||
plate_step_17 = 17
|
||||
plate_step_18 = 18
|
||||
plate_step_boost = 117, 118, 218
|
||||
plate_step_boost_2 = 217
|
||||
missing2none = -9999
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -76,7 +76,8 @@
|
||||
"plate_step_16": "mdi:circle-slice-7",
|
||||
"plate_step_17": "mdi:circle-slice-8",
|
||||
"plate_step_18": "mdi:circle-slice-8",
|
||||
"plate_step_boost": "mdi:alpha-b-circle-outline"
|
||||
"plate_step_boost": "mdi:alpha-b-circle-outline",
|
||||
"plate_step_boost_2": "mdi:alpha-b-circle"
|
||||
}
|
||||
},
|
||||
"program_type": {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pymiele"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pymiele==0.5.2"],
|
||||
"requirements": ["pymiele==0.5.4"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_mieleathome._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -731,7 +731,7 @@ class MielePlateSensor(MieleSensor):
|
||||
)
|
||||
).name
|
||||
if self.device.state_plate_step
|
||||
else PlatePowerStep.plate_step_0
|
||||
else PlatePowerStep.plate_step_0.name
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse:
|
||||
else {}
|
||||
),
|
||||
}
|
||||
if item["parameters"]
|
||||
if item.get("parameters")
|
||||
else {}
|
||||
),
|
||||
}
|
||||
|
||||
@@ -203,27 +203,28 @@
|
||||
"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",
|
||||
"plate_step_boost_2": "Boost 2"
|
||||
}
|
||||
},
|
||||
"drying_step": {
|
||||
@@ -485,6 +486,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 +830,7 @@
|
||||
"rice_pudding_steam_cooking": "Rice pudding (steam cooking)",
|
||||
"rinse": "Rinse",
|
||||
"rinse_out_lint": "Rinse out lint",
|
||||
"rinse_salt": "Rinse salt",
|
||||
"risotto": "Risotto",
|
||||
"ristretto": "Ristretto",
|
||||
"roast_beef_low_temperature_cooking": "Roast beef (low temperature cooking)",
|
||||
|
||||
@@ -43,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
historic_data_coordinator = MillHistoricDataUpdateCoordinator(
|
||||
hass,
|
||||
entry,
|
||||
mill_data_connection=mill_data_connection,
|
||||
)
|
||||
historic_data_coordinator.async_add_listener(lambda: None)
|
||||
|
||||
@@ -60,6 +60,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
*,
|
||||
mill_data_connection: Mill,
|
||||
) -> None:
|
||||
@@ -70,6 +71,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="MillHistoricDataUpdateCoordinator",
|
||||
config_entry=config_entry,
|
||||
)
|
||||
|
||||
async def _async_update_data(self):
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["motionblinds"],
|
||||
"requirements": ["motionblinds==0.6.29"]
|
||||
"requirements": ["motionblinds==0.6.30"]
|
||||
}
|
||||
|
||||
@@ -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%]",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mystrom",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pymystrom"],
|
||||
"requirements": ["python-mystrom==2.4.0"]
|
||||
"requirements": ["python-mystrom==2.5.0"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@ from typing import Any, Final, cast
|
||||
from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_USER,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_TOKEN
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.json import save_json
|
||||
@@ -200,7 +205,9 @@ class NanoleafConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="unknown")
|
||||
name = self.nanoleaf.name
|
||||
|
||||
await self.async_set_unique_id(name)
|
||||
await self.async_set_unique_id(
|
||||
name, raise_on_progress=self.source != SOURCE_USER
|
||||
)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: self.nanoleaf.host})
|
||||
|
||||
if discovery_integration_import:
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyatmo"],
|
||||
"requirements": ["pyatmo==9.2.1"]
|
||||
"requirements": ["pyatmo==9.2.3"]
|
||||
}
|
||||
|
||||
@@ -92,11 +92,15 @@ async def async_update_options(hass: HomeAssistant, entry: OllamaConfigEntry) ->
|
||||
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] = {}
|
||||
url_entries: dict[str, tuple[ConfigEntry, bool]] = {}
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
@@ -112,33 +116,64 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
title=entry.title,
|
||||
unique_id=None,
|
||||
)
|
||||
if entry.data[CONF_URL] not in api_keys_entries:
|
||||
if entry.data[CONF_URL] not in url_entries:
|
||||
use_existing = True
|
||||
api_keys_entries[entry.data[CONF_URL]] = entry
|
||||
all_disabled = all(
|
||||
e.disabled_by is not None
|
||||
for e in entries
|
||||
if e.data[CONF_URL] == entry.data[CONF_URL]
|
||||
)
|
||||
url_entries[entry.data[CONF_URL]] = (entry, all_disabled)
|
||||
|
||||
parent_entry = api_keys_entries[entry.data[CONF_URL]]
|
||||
parent_entry, all_disabled = url_entries[entry.data[CONF_URL]]
|
||||
|
||||
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,
|
||||
@@ -158,6 +193,7 @@ 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,
|
||||
@@ -165,7 +201,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
data={CONF_URL: entry.data[CONF_URL]},
|
||||
options={},
|
||||
version=3,
|
||||
minor_version=1,
|
||||
minor_version=3,
|
||||
)
|
||||
|
||||
|
||||
@@ -211,32 +247,69 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OllamaConfigEntry) ->
|
||||
)
|
||||
|
||||
if entry.version == 3 and entry.minor_version == 1:
|
||||
# Add AI Task subentry with default options. We can only create a new
|
||||
# subentry if we can find an existing model in the entry. The model
|
||||
# was removed in the previous migration step, so we need to
|
||||
# check the subentries for an existing model.
|
||||
existing_model = next(
|
||||
iter(
|
||||
model
|
||||
for subentry in entry.subentries.values()
|
||||
if (model := subentry.data.get(CONF_MODEL)) is not None
|
||||
),
|
||||
None,
|
||||
)
|
||||
if existing_model:
|
||||
hass.config_entries.async_add_subentry(
|
||||
entry,
|
||||
ConfigSubentry(
|
||||
data=MappingProxyType({CONF_MODEL: existing_model}),
|
||||
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=2)
|
||||
|
||||
if entry.version == 3 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
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _add_ai_task_subentry(hass: HomeAssistant, entry: OllamaConfigEntry) -> None:
|
||||
"""Add AI Task subentry to the config entry."""
|
||||
# Add AI Task subentry with default options. We can only create a new
|
||||
# subentry if we can find an existing model in the entry. The model
|
||||
# was removed in the previous migration step, so we need to
|
||||
# check the subentries for an existing model.
|
||||
existing_model = next(
|
||||
iter(
|
||||
model
|
||||
for subentry in entry.subentries.values()
|
||||
if (model := subentry.data.get(CONF_MODEL)) is not None
|
||||
),
|
||||
None,
|
||||
)
|
||||
if existing_model:
|
||||
hass.config_entries.async_add_subentry(
|
||||
entry,
|
||||
ConfigSubentry(
|
||||
data=MappingProxyType({CONF_MODEL: existing_model}),
|
||||
subentry_type="ai_task_data",
|
||||
title=DEFAULT_AI_TASK_NAME,
|
||||
unique_id=None,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -76,7 +76,7 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Ollama."""
|
||||
|
||||
VERSION = 3
|
||||
MINOR_VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize config flow."""
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user