mirror of
https://github.com/home-assistant/core.git
synced 2025-09-20 18:39:40 +00:00
Compare commits
120 Commits
trigger_de
...
2025.8.0b4
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 |
@@ -6,11 +6,11 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from airos.exceptions import (
|
||||
ConnectionAuthenticationError,
|
||||
ConnectionSetupError,
|
||||
DataMissingError,
|
||||
DeviceConnectionError,
|
||||
KeyDataMissingError,
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
AirOSKeyDataMissingError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -59,13 +59,13 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
airos_data = await airos_device.status()
|
||||
|
||||
except (
|
||||
ConnectionSetupError,
|
||||
DeviceConnectionError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
):
|
||||
errors["base"] = "cannot_connect"
|
||||
except (ConnectionAuthenticationError, DataMissingError):
|
||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||
errors["base"] = "invalid_auth"
|
||||
except KeyDataMissingError:
|
||||
except AirOSKeyDataMissingError:
|
||||
errors["base"] = "key_data_missing"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
|
@@ -6,10 +6,10 @@ import logging
|
||||
|
||||
from airos.airos8 import AirOS, AirOSData
|
||||
from airos.exceptions import (
|
||||
ConnectionAuthenticationError,
|
||||
ConnectionSetupError,
|
||||
DataMissingError,
|
||||
DeviceConnectionError,
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -47,18 +47,22 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
||||
try:
|
||||
await self.airos_device.login()
|
||||
return await self.airos_device.status()
|
||||
except (ConnectionAuthenticationError,) as err:
|
||||
except (AirOSConnectionAuthenticationError,) as err:
|
||||
_LOGGER.exception("Error authenticating with airOS device")
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from err
|
||||
except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err:
|
||||
except (
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
TimeoutError,
|
||||
) as err:
|
||||
_LOGGER.error("Error connecting to airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
except (DataMissingError,) as err:
|
||||
except (AirOSDataMissingError,) as err:
|
||||
_LOGGER.error("Expected data not returned by airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
33
homeassistant/components/airos/diagnostics.py
Normal file
33
homeassistant/components/airos/diagnostics.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Diagnostics support for airOS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AirOSConfigEntry
|
||||
|
||||
IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related
|
||||
HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address
|
||||
TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD]
|
||||
TO_REDACT_AIROS = [
|
||||
"hostname", # Prevent leaking device naming
|
||||
"essid", # Network SSID
|
||||
"lat", # GPS latitude to prevent exposing location data.
|
||||
"lon", # GPS longitude to prevent exposing location data.
|
||||
*HW_REDACT,
|
||||
*IP_REDACT,
|
||||
]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AirOSConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {
|
||||
"entry_data": async_redact_data(entry.data, TO_REDACT_HA),
|
||||
"data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS),
|
||||
}
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["airos==0.2.1"]
|
||||
"requirements": ["airos==0.2.4"]
|
||||
}
|
||||
|
@@ -41,7 +41,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: done
|
||||
|
@@ -69,13 +69,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
translation_key="wireless_essid",
|
||||
value_fn=lambda data: data.wireless.essid,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_mode",
|
||||
translation_key="wireless_mode",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(),
|
||||
options=WIRELESS_MODE_OPTIONS,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_antenna_gain",
|
||||
translation_key="wireless_antenna_gain",
|
||||
|
@@ -43,13 +43,6 @@
|
||||
"wireless_essid": {
|
||||
"name": "Wireless SSID"
|
||||
},
|
||||
"wireless_mode": {
|
||||
"name": "Wireless mode",
|
||||
"state": {
|
||||
"ap_ptp": "Access point",
|
||||
"sta_ptp": "Station"
|
||||
}
|
||||
},
|
||||
"wireless_antenna_gain": {
|
||||
"name": "Antenna gain"
|
||||
},
|
||||
|
@@ -7,21 +7,18 @@ import logging
|
||||
|
||||
from airthings import Airthings
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_SECRET
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
|
||||
"""Set up Airthings from a config entry."""
|
||||
@@ -31,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
@@ -5,6 +5,7 @@ import logging
|
||||
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -13,15 +14,23 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
|
||||
"""Coordinator for Airthings data updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
airthings: Airthings,
|
||||
config_entry: AirthingsConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_method=self._update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
|
@@ -430,7 +430,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
"model": device.model,
|
||||
"sw_version": device.sw_version,
|
||||
"hw_version": device.hw_version,
|
||||
"has_suggested_area": device.suggested_area is not None,
|
||||
"has_configuration_url": device.configuration_url is not None,
|
||||
"via_device": None,
|
||||
}
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -20,7 +20,7 @@
|
||||
"bluetooth-adapters==2.0.0",
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.2",
|
||||
"dbus-fast==2.44.2",
|
||||
"habluetooth==4.0.1"
|
||||
"dbus-fast==2.44.3",
|
||||
"habluetooth==4.0.2"
|
||||
]
|
||||
}
|
||||
|
@@ -64,6 +64,7 @@ class BroadlinkUpdateManager(ABC, Generic[_ApiT]):
|
||||
device.hass,
|
||||
_LOGGER,
|
||||
name=f"{device.name} ({device.api.model} at {device.api.host[0]})",
|
||||
config_entry=device.config,
|
||||
update_method=self.async_update,
|
||||
update_interval=self.SCAN_INTERVAL,
|
||||
)
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.3.1"]
|
||||
}
|
||||
|
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.110.0"],
|
||||
"requirements": ["hass-nabucasa==0.111.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.23"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.7.30"]
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["denonavr"],
|
||||
"requirements": ["denonavr==1.1.1"],
|
||||
"requirements": ["denonavr==1.1.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Denon",
|
||||
|
@@ -16,7 +16,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.2.0",
|
||||
"aiodiscover==2.7.0",
|
||||
"aiodiscover==2.7.1",
|
||||
"cached-ipaddress==0.10.0"
|
||||
]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -116,6 +116,8 @@ async def async_get_config_entry_diagnostics(
|
||||
entities.append({"entity": entity_dict, "state": state_dict})
|
||||
device_dict = asdict(device)
|
||||
device_dict.pop("_cache", None)
|
||||
# This can be removed when suggested_area is removed from DeviceEntry
|
||||
device_dict.pop("_suggested_area")
|
||||
device_entities.append({"device": device_dict, "entities": entities})
|
||||
|
||||
# remove envoy serial
|
||||
|
@@ -316,10 +316,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# Don't call _fetch_device_info() for ignored entries
|
||||
raise AbortFlow("already_configured")
|
||||
configured_host: str | None = entry.data.get(CONF_HOST)
|
||||
configured_port: int | None = entry.data.get(CONF_PORT)
|
||||
if configured_host == host and configured_port == port:
|
||||
configured_port: int = entry.data.get(CONF_PORT, DEFAULT_PORT)
|
||||
# When port is None (from DHCP discovery), only compare hosts
|
||||
if configured_host == host and (port is None or configured_port == port):
|
||||
# Don't probe to verify the mac is correct since
|
||||
# the host and port matches.
|
||||
# the host matches (and port matches if provided).
|
||||
raise AbortFlow("already_configured")
|
||||
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
|
||||
await self._fetch_device_info(host, port or configured_port, configured_psk)
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==37.1.5",
|
||||
"aioesphomeapi==37.2.2",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.1.0"
|
||||
],
|
||||
|
@@ -106,6 +106,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_logger_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
await self.logger_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -120,6 +121,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_meters_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -129,6 +131,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_ohmpilot_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -138,6 +141,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_power_flow_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -147,6 +151,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_storages_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -206,6 +211,7 @@ class FroniusSolarNet:
|
||||
logger=_LOGGER,
|
||||
name=_inverter_name,
|
||||
inverter_info=_inverter_info,
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
await _coordinator.async_refresh()
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250730.0"]
|
||||
"requirements": ["home-assistant-frontend==20250805.0"]
|
||||
}
|
||||
|
@@ -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": {
|
||||
|
@@ -86,9 +86,11 @@ UNSUPPORTED_REASONS = {
|
||||
UNSUPPORTED_SKIP_REPAIR = {"privileged"}
|
||||
UNHEALTHY_REASONS = {
|
||||
"docker",
|
||||
"supervisor",
|
||||
"setup",
|
||||
"duplicate_os_installation",
|
||||
"oserror_bad_message",
|
||||
"privileged",
|
||||
"setup",
|
||||
"supervisor",
|
||||
"untrusted",
|
||||
}
|
||||
|
||||
|
@@ -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": {
|
||||
|
@@ -12,6 +12,7 @@ from ha_silabs_firmware_client import (
|
||||
ManifestMissing,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -24,13 +25,20 @@ FIRMWARE_REFRESH_INTERVAL = timedelta(hours=8)
|
||||
class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]):
|
||||
"""Coordinator to manage firmware updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, session: ClientSession, url: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
session: ClientSession,
|
||||
url: str,
|
||||
) -> None:
|
||||
"""Initialize the firmware update coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="firmware update coordinator",
|
||||
update_interval=FIRMWARE_REFRESH_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
self.hass = hass
|
||||
self.session = session
|
||||
|
@@ -124,6 +124,7 @@ def _async_create_update_entity(
|
||||
config_entry=config_entry,
|
||||
update_coordinator=FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
config_entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
),
|
||||
|
@@ -129,6 +129,7 @@ def _async_create_update_entity(
|
||||
config_entry=config_entry,
|
||||
update_coordinator=FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
config_entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
),
|
||||
|
@@ -163,6 +163,7 @@ async def async_setup_entry(
|
||||
name="light",
|
||||
update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update),
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
request_refresh_debouncer=Debouncer(
|
||||
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||
),
|
||||
@@ -197,6 +198,7 @@ async def async_setup_entry(
|
||||
name="group",
|
||||
update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update),
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
request_refresh_debouncer=Debouncer(
|
||||
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||
),
|
||||
|
@@ -53,6 +53,7 @@ class SensorManager:
|
||||
LOGGER,
|
||||
name="sensor",
|
||||
update_method=self.async_update_data,
|
||||
config_entry=bridge.config_entry,
|
||||
update_interval=self.SCAN_INTERVAL,
|
||||
request_refresh_debouncer=debounce.Debouncer(
|
||||
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2.1.1"]
|
||||
"requirements": ["aioautomower==2.1.2"]
|
||||
}
|
||||
|
@@ -71,10 +71,10 @@ ERROR_KEYS = [
|
||||
"cutting_drive_motor_2_defect",
|
||||
"cutting_drive_motor_3_defect",
|
||||
"cutting_height_blocked",
|
||||
"cutting_height_problem",
|
||||
"cutting_height_problem_curr",
|
||||
"cutting_height_problem_dir",
|
||||
"cutting_height_problem_drive",
|
||||
"cutting_height_problem",
|
||||
"cutting_motor_problem",
|
||||
"cutting_stopped_slope_too_steep",
|
||||
"cutting_system_blocked",
|
||||
@@ -117,7 +117,6 @@ ERROR_KEYS = [
|
||||
"no_accurate_position_from_satellites",
|
||||
"no_confirmed_position",
|
||||
"no_drive",
|
||||
"no_error",
|
||||
"no_loop_signal",
|
||||
"no_power_in_charging_station",
|
||||
"no_response_from_charger",
|
||||
@@ -169,8 +168,8 @@ ERROR_KEYS = [
|
||||
]
|
||||
|
||||
|
||||
ERROR_KEY_LIST = list(
|
||||
dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES])
|
||||
ERROR_KEY_LIST = sorted(
|
||||
set(ERROR_KEYS) | {state.lower() for state in ERROR_STATES} | {"no_error"}
|
||||
)
|
||||
|
||||
INACTIVE_REASONS: list = [
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["imgw_pib==1.5.1"]
|
||||
"requirements": ["imgw_pib==1.5.2"]
|
||||
}
|
||||
|
@@ -99,7 +99,7 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
),
|
||||
}
|
||||
)
|
||||
self.add_suggested_values_to_schema(
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
data_schema,
|
||||
{"section_1": {"int": self.config_entry.options.get(CONF_INT, 10)}},
|
||||
)
|
||||
|
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.8.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.7.23.50952"
|
||||
"knx-frontend==2025.8.6.52906"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -135,6 +135,7 @@ class KrakenData:
|
||||
self._hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
config_entry=self._config_entry,
|
||||
update_method=self.async_update,
|
||||
update_interval=timedelta(
|
||||
seconds=self._config_entry.options[CONF_SCAN_INTERVAL]
|
||||
|
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pylitterbot==2024.2.2"]
|
||||
"requirements": ["pylitterbot==2024.2.3"]
|
||||
}
|
||||
|
@@ -285,7 +285,9 @@ DISCOVERY_SCHEMAS = [
|
||||
native_min_value=0.5,
|
||||
native_step=0.5,
|
||||
device_to_ha=(
|
||||
lambda x: None if x is None else x / 2 # Matter range (1-200)
|
||||
lambda x: None
|
||||
if x is None
|
||||
else min(x, 200) / 2 # Matter range (1-200, capped at 200)
|
||||
),
|
||||
ha_to_device=lambda x: round(x * 2), # HA range 0.5–100.0%
|
||||
mode=NumberMode.SLIDER,
|
||||
|
@@ -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,
|
||||
),
|
||||
|
@@ -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.07.21"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -63,6 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"Météo-France forecast for city {entry.title}",
|
||||
config_entry=entry,
|
||||
update_method=_async_update_data_forecast_forecast,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
@@ -80,6 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"Météo-France rain for city {entry.title}",
|
||||
config_entry=entry,
|
||||
update_method=_async_update_data_rain,
|
||||
update_interval=SCAN_INTERVAL_RAIN,
|
||||
)
|
||||
@@ -103,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"Météo-France alert for department {department}",
|
||||
config_entry=entry,
|
||||
update_method=_async_update_data_alert,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
@@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> boo
|
||||
) from err
|
||||
|
||||
# Setup MieleAPI and coordinator for data fetch
|
||||
coordinator = MieleDataUpdateCoordinator(hass, auth)
|
||||
coordinator = MieleDataUpdateCoordinator(hass, entry, auth)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
|
@@ -431,6 +431,16 @@ DISHWASHER_PROGRAM_ID: dict[int, str] = {
|
||||
38: "quick_power_wash",
|
||||
42: "tall_items",
|
||||
44: "power_wash",
|
||||
200: "eco",
|
||||
202: "automatic",
|
||||
203: "comfort_wash",
|
||||
204: "power_wash",
|
||||
205: "intensive",
|
||||
207: "extra_quiet",
|
||||
209: "comfort_wash_plus",
|
||||
210: "gentle",
|
||||
214: "maintenance",
|
||||
215: "rinse_salt",
|
||||
}
|
||||
TUMBLE_DRYER_PROGRAM_ID: dict[int, str] = {
|
||||
-1: "no_program", # Extrapolated from other device types.
|
||||
|
@@ -42,12 +42,14 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MieleConfigEntry,
|
||||
api: AsyncConfigEntryAuth,
|
||||
) -> None:
|
||||
"""Initialize the Miele data coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=120),
|
||||
)
|
||||
|
@@ -731,7 +731,7 @@ class MielePlateSensor(MieleSensor):
|
||||
)
|
||||
).name
|
||||
if self.device.state_plate_step
|
||||
else PlatePowerStep.plate_step_0
|
||||
else PlatePowerStep.plate_step_0.name
|
||||
)
|
||||
|
||||
|
||||
|
@@ -203,7 +203,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse:
|
||||
else {}
|
||||
),
|
||||
}
|
||||
if item["parameters"]
|
||||
if item.get("parameters")
|
||||
else {}
|
||||
),
|
||||
}
|
||||
|
@@ -203,27 +203,27 @@
|
||||
"plate": {
|
||||
"name": "Plate {plate_no}",
|
||||
"state": {
|
||||
"power_step_0": "0",
|
||||
"power_step_warm": "Warming",
|
||||
"power_step_1": "1",
|
||||
"power_step_2": "1\u2022",
|
||||
"power_step_3": "2",
|
||||
"power_step_4": "2\u2022",
|
||||
"power_step_5": "3",
|
||||
"power_step_6": "3\u2022",
|
||||
"power_step_7": "4",
|
||||
"power_step_8": "4\u2022",
|
||||
"power_step_9": "5",
|
||||
"power_step_10": "5\u2022",
|
||||
"power_step_11": "6",
|
||||
"power_step_12": "6\u2022",
|
||||
"power_step_13": "7",
|
||||
"power_step_14": "7\u2022",
|
||||
"power_step_15": "8",
|
||||
"power_step_16": "8\u2022",
|
||||
"power_step_17": "9",
|
||||
"power_step_18": "9\u2022",
|
||||
"power_step_boost": "Boost"
|
||||
"plate_step_0": "0",
|
||||
"plate_step_warm": "Warming",
|
||||
"plate_step_1": "1",
|
||||
"plate_step_2": "1\u2022",
|
||||
"plate_step_3": "2",
|
||||
"plate_step_4": "2\u2022",
|
||||
"plate_step_5": "3",
|
||||
"plate_step_6": "3\u2022",
|
||||
"plate_step_7": "4",
|
||||
"plate_step_8": "4\u2022",
|
||||
"plate_step_9": "5",
|
||||
"plate_step_10": "5\u2022",
|
||||
"plate_step_11": "6",
|
||||
"plate_step_12": "6\u2022",
|
||||
"plate_step_13": "7",
|
||||
"plate_step_14": "7\u2022",
|
||||
"plate_step_15": "8",
|
||||
"plate_step_16": "8\u2022",
|
||||
"plate_step_17": "9",
|
||||
"plate_step_18": "9\u2022",
|
||||
"plate_step_boost": "Boost"
|
||||
}
|
||||
},
|
||||
"drying_step": {
|
||||
@@ -485,6 +485,8 @@
|
||||
"cook_bacon": "Cook bacon",
|
||||
"biscuits_short_crust_pastry_1_tray": "Biscuits, short crust pastry (1 tray)",
|
||||
"biscuits_short_crust_pastry_2_trays": "Biscuits, short crust pastry (2 trays)",
|
||||
"comfort_wash": "Comfort wash",
|
||||
"comfort_wash_plus": "Comfort wash plus",
|
||||
"cool_air": "Cool air",
|
||||
"corn_on_the_cob": "Corn on the cob",
|
||||
"cottons": "Cottons",
|
||||
@@ -827,6 +829,7 @@
|
||||
"rice_pudding_steam_cooking": "Rice pudding (steam cooking)",
|
||||
"rinse": "Rinse",
|
||||
"rinse_out_lint": "Rinse out lint",
|
||||
"rinse_salt": "Rinse salt",
|
||||
"risotto": "Risotto",
|
||||
"ristretto": "Ristretto",
|
||||
"roast_beef_low_temperature_cooking": "Roast beef (low temperature cooking)",
|
||||
|
@@ -43,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
historic_data_coordinator = MillHistoricDataUpdateCoordinator(
|
||||
hass,
|
||||
entry,
|
||||
mill_data_connection=mill_data_connection,
|
||||
)
|
||||
historic_data_coordinator.async_add_listener(lambda: None)
|
||||
|
@@ -60,6 +60,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
*,
|
||||
mill_data_connection: Mill,
|
||||
) -> None:
|
||||
@@ -70,6 +71,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="MillHistoricDataUpdateCoordinator",
|
||||
config_entry=config_entry,
|
||||
)
|
||||
|
||||
async def _async_update_data(self):
|
||||
|
@@ -21,5 +21,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["motionblinds"],
|
||||
"requirements": ["motionblinds==0.6.29"]
|
||||
"requirements": ["motionblinds==0.6.30"]
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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": {
|
||||
|
@@ -9,5 +9,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["openai==1.93.3", "python-open-router==0.3.0"]
|
||||
"requirements": ["openai==1.93.3", "python-open-router==0.3.1"]
|
||||
}
|
||||
|
@@ -52,9 +52,9 @@
|
||||
}
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "Add Generate data with AI service"
|
||||
"user": "Add AI task"
|
||||
},
|
||||
"entry_type": "Generate data with AI service",
|
||||
"entry_type": "AI task",
|
||||
"abort": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
|
@@ -272,11 +272,15 @@ async def async_update_options(hass: HomeAssistant, entry: OpenAIConfigEntry) ->
|
||||
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
"""Migrate integration entry structure."""
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
# Make sure we get enabled config entries first
|
||||
entries = sorted(
|
||||
hass.config_entries.async_entries(DOMAIN),
|
||||
key=lambda e: e.disabled_by is not None,
|
||||
)
|
||||
if not any(entry.version == 1 for entry in entries):
|
||||
return
|
||||
|
||||
api_keys_entries: dict[str, ConfigEntry] = {}
|
||||
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
@@ -290,30 +294,61 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
)
|
||||
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
||||
use_existing = True
|
||||
api_keys_entries[entry.data[CONF_API_KEY]] = entry
|
||||
all_disabled = all(
|
||||
e.disabled_by is not None
|
||||
for e in entries
|
||||
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
|
||||
)
|
||||
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
|
||||
|
||||
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
|
||||
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
|
||||
|
||||
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
||||
conversation_entity = entity_registry.async_get_entity_id(
|
||||
conversation_entity_id = entity_registry.async_get_entity_id(
|
||||
"conversation",
|
||||
DOMAIN,
|
||||
entry.entry_id,
|
||||
)
|
||||
if conversation_entity is not None:
|
||||
entity_registry.async_update_entity(
|
||||
conversation_entity,
|
||||
config_entry_id=parent_entry.entry_id,
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
new_unique_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, entry.entry_id)}
|
||||
)
|
||||
|
||||
if conversation_entity_id is not None:
|
||||
conversation_entity_entry = entity_registry.entities[conversation_entity_id]
|
||||
entity_disabled_by = conversation_entity_entry.disabled_by
|
||||
if (
|
||||
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
|
||||
and not all_disabled
|
||||
):
|
||||
# Device and entity registries don't update the disabled_by flag
|
||||
# when moving a device or entity from one config entry to another,
|
||||
# so we need to do it manually.
|
||||
entity_disabled_by = (
|
||||
er.RegistryEntryDisabler.DEVICE
|
||||
if device
|
||||
else er.RegistryEntryDisabler.USER
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
conversation_entity_id,
|
||||
config_entry_id=parent_entry.entry_id,
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
disabled_by=entity_disabled_by,
|
||||
new_unique_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
if device is not None:
|
||||
# Device and entity registries don't update the disabled_by flag when
|
||||
# moving a device or entity from one config entry to another, so we
|
||||
# need to do it manually.
|
||||
device_disabled_by = device.disabled_by
|
||||
if (
|
||||
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
|
||||
and not all_disabled
|
||||
):
|
||||
device_disabled_by = dr.DeviceEntryDisabler.USER
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
disabled_by=device_disabled_by,
|
||||
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
||||
add_config_subentry_id=subentry.subentry_id,
|
||||
add_config_entry_id=parent_entry.entry_id,
|
||||
@@ -333,12 +368,13 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
if not use_existing:
|
||||
await hass.config_entries.async_remove(entry.entry_id)
|
||||
else:
|
||||
_add_ai_task_subentry(hass, entry)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
title=DEFAULT_NAME,
|
||||
options={},
|
||||
version=2,
|
||||
minor_version=2,
|
||||
minor_version=4,
|
||||
)
|
||||
|
||||
|
||||
@@ -365,19 +401,56 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) ->
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 2:
|
||||
hass.config_entries.async_add_subentry(
|
||||
entry,
|
||||
ConfigSubentry(
|
||||
data=MappingProxyType(RECOMMENDED_AI_TASK_OPTIONS),
|
||||
subentry_type="ai_task_data",
|
||||
title=DEFAULT_AI_TASK_NAME,
|
||||
unique_id=None,
|
||||
),
|
||||
)
|
||||
_add_ai_task_subentry(hass, entry)
|
||||
hass.config_entries.async_update_entry(entry, minor_version=3)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 3:
|
||||
# Fix migration where the disabled_by flag was not set correctly.
|
||||
# We can currently only correct this for enabled config entries,
|
||||
# because migration does not run for disabled config entries. This
|
||||
# is asserted in tests, and if that behavior is changed, we should
|
||||
# correct also disabled config entries.
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, entry.entry_id
|
||||
)
|
||||
if entry.disabled_by is None:
|
||||
# If the config entry is not disabled, we need to set the disabled_by
|
||||
# flag on devices to USER, and on entities to DEVICE, if they are set
|
||||
# to CONFIG_ENTRY.
|
||||
for device in devices:
|
||||
if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY:
|
||||
continue
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
disabled_by=dr.DeviceEntryDisabler.USER,
|
||||
)
|
||||
for entity in entity_entries:
|
||||
if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY:
|
||||
continue
|
||||
entity_registry.async_update_entity(
|
||||
entity.entity_id,
|
||||
disabled_by=er.RegistryEntryDisabler.DEVICE,
|
||||
)
|
||||
hass.config_entries.async_update_entry(entry, minor_version=4)
|
||||
|
||||
LOGGER.debug(
|
||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _add_ai_task_subentry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> None:
|
||||
"""Add AI Task subentry to the config entry."""
|
||||
hass.config_entries.async_add_subentry(
|
||||
entry,
|
||||
ConfigSubentry(
|
||||
data=MappingProxyType(RECOMMENDED_AI_TASK_OPTIONS),
|
||||
subentry_type="ai_task_data",
|
||||
title=DEFAULT_AI_TASK_NAME,
|
||||
unique_id=None,
|
||||
),
|
||||
)
|
||||
|
@@ -98,7 +98,7 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for OpenAI Conversation."""
|
||||
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
MINOR_VERSION = 4
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
@@ -73,10 +73,10 @@
|
||||
},
|
||||
"ai_task_data": {
|
||||
"initiate_flow": {
|
||||
"user": "Add Generate data with AI service",
|
||||
"reconfigure": "Reconfigure Generate data with AI service"
|
||||
"user": "Add AI task",
|
||||
"reconfigure": "Reconfigure AI task"
|
||||
},
|
||||
"entry_type": "Generate data with AI service",
|
||||
"entry_type": "AI task",
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
|
@@ -9,6 +9,8 @@ from typing import Any
|
||||
from opower import (
|
||||
CannotConnect,
|
||||
InvalidAuth,
|
||||
MfaChallenge,
|
||||
MfaHandlerBase,
|
||||
Opower,
|
||||
create_cookie_jar,
|
||||
get_supported_utility_names,
|
||||
@@ -16,49 +18,34 @@ from opower import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN
|
||||
from .const import CONF_LOGIN_DATA, CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names()),
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
CONF_MFA_CODE = "mfa_code"
|
||||
CONF_MFA_METHOD = "mfa_method"
|
||||
|
||||
|
||||
async def _validate_login(
|
||||
hass: HomeAssistant, login_data: dict[str, str]
|
||||
) -> dict[str, str]:
|
||||
"""Validate login data and return any errors."""
|
||||
hass: HomeAssistant,
|
||||
data: Mapping[str, Any],
|
||||
) -> None:
|
||||
"""Validate login data and raise exceptions on failure."""
|
||||
api = Opower(
|
||||
async_create_clientsession(hass, cookie_jar=create_cookie_jar()),
|
||||
login_data[CONF_UTILITY],
|
||||
login_data[CONF_USERNAME],
|
||||
login_data[CONF_PASSWORD],
|
||||
login_data.get(CONF_TOTP_SECRET),
|
||||
data[CONF_UTILITY],
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
data.get(CONF_TOTP_SECRET),
|
||||
data.get(CONF_LOGIN_DATA),
|
||||
)
|
||||
errors: dict[str, str] = {}
|
||||
try:
|
||||
await api.async_login()
|
||||
except InvalidAuth:
|
||||
_LOGGER.exception(
|
||||
"Invalid auth when connecting to %s", login_data[CONF_UTILITY]
|
||||
)
|
||||
errors["base"] = "invalid_auth"
|
||||
except CannotConnect:
|
||||
_LOGGER.exception("Could not connect to %s", login_data[CONF_UTILITY])
|
||||
errors["base"] = "cannot_connect"
|
||||
return errors
|
||||
await api.async_login()
|
||||
|
||||
|
||||
class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@@ -68,81 +55,147 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize a new OpowerConfigFlow."""
|
||||
self.utility_info: dict[str, Any] | None = None
|
||||
self._data: dict[str, Any] = {}
|
||||
self.mfa_handler: MfaHandlerBase | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
"""Handle the initial step (select utility)."""
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_UTILITY: user_input[CONF_UTILITY],
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
}
|
||||
)
|
||||
if select_utility(user_input[CONF_UTILITY]).accepts_mfa():
|
||||
self.utility_info = user_input
|
||||
return await self.async_step_mfa()
|
||||
self._data[CONF_UTILITY] = user_input[CONF_UTILITY]
|
||||
return await self.async_step_credentials()
|
||||
|
||||
errors = await _validate_login(self.hass, user_input)
|
||||
if not errors:
|
||||
return self._async_create_opower_entry(user_input)
|
||||
else:
|
||||
user_input = {}
|
||||
user_input.pop(CONF_PASSWORD, None)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names())}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_credentials(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle credentials step."""
|
||||
errors: dict[str, str] = {}
|
||||
utility = select_utility(self._data[CONF_UTILITY])
|
||||
|
||||
if user_input is not None:
|
||||
self._data.update(user_input)
|
||||
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_UTILITY: self._data[CONF_UTILITY],
|
||||
CONF_USERNAME: self._data[CONF_USERNAME],
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
await _validate_login(self.hass, self._data)
|
||||
except MfaChallenge as exc:
|
||||
self.mfa_handler = exc.handler
|
||||
return await self.async_step_mfa_options()
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self._async_create_opower_entry(self._data)
|
||||
|
||||
schema_dict: VolDictType = {
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
if utility.accepts_totp_secret():
|
||||
schema_dict[vol.Optional(CONF_TOTP_SECRET)] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="credentials",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA, user_input
|
||||
vol.Schema(schema_dict), user_input
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_mfa(
|
||||
async def async_step_mfa_options(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle MFA step."""
|
||||
assert self.utility_info is not None
|
||||
"""Handle MFA options step."""
|
||||
errors: dict[str, str] = {}
|
||||
assert self.mfa_handler is not None
|
||||
|
||||
if user_input is not None:
|
||||
method = user_input[CONF_MFA_METHOD]
|
||||
try:
|
||||
await self.mfa_handler.async_select_mfa_option(method)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return await self.async_step_mfa_code()
|
||||
|
||||
mfa_options = await self.mfa_handler.async_get_mfa_options()
|
||||
if not mfa_options:
|
||||
return await self.async_step_mfa_code()
|
||||
return self.async_show_form(
|
||||
step_id="mfa_options",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema({vol.Required(CONF_MFA_METHOD): vol.In(mfa_options)}),
|
||||
user_input,
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_mfa_code(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle MFA code submission step."""
|
||||
assert self.mfa_handler is not None
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
data = {**self.utility_info, **user_input}
|
||||
errors = await _validate_login(self.hass, data)
|
||||
if not errors:
|
||||
return self._async_create_opower_entry(data)
|
||||
|
||||
if errors:
|
||||
schema = {
|
||||
vol.Required(
|
||||
CONF_USERNAME, default=self.utility_info[CONF_USERNAME]
|
||||
): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
else:
|
||||
schema = {}
|
||||
|
||||
schema[vol.Required(CONF_TOTP_SECRET)] = str
|
||||
code = user_input[CONF_MFA_CODE]
|
||||
try:
|
||||
login_data = await self.mfa_handler.async_submit_mfa_code(code)
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_mfa_code"
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
self._data[CONF_LOGIN_DATA] = login_data
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), data=self._data
|
||||
)
|
||||
return self._async_create_opower_entry(self._data)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="mfa",
|
||||
data_schema=vol.Schema(schema),
|
||||
step_id="mfa_code",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema({vol.Required(CONF_MFA_CODE): str}), user_input
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_create_opower_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
def _async_create_opower_entry(
|
||||
self, data: dict[str, Any], **kwargs: Any
|
||||
) -> ConfigFlowResult:
|
||||
"""Create the config entry."""
|
||||
return self.async_create_entry(
|
||||
title=f"{data[CONF_UTILITY]} ({data[CONF_USERNAME]})",
|
||||
data=data,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle configuration by re-auth."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
self._data = dict(reauth_entry.data)
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders={CONF_NAME: reauth_entry.title},
|
||||
)
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -150,21 +203,34 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
errors: dict[str, str] = {}
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
if user_input is not None:
|
||||
data = {**reauth_entry.data, **user_input}
|
||||
errors = await _validate_login(self.hass, data)
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(reauth_entry, data=data)
|
||||
|
||||
schema: VolDictType = {
|
||||
vol.Required(CONF_USERNAME): reauth_entry.data[CONF_USERNAME],
|
||||
if user_input is not None:
|
||||
self._data.update(user_input)
|
||||
try:
|
||||
await _validate_login(self.hass, self._data)
|
||||
except MfaChallenge as exc:
|
||||
self.mfa_handler = exc.handler
|
||||
return await self.async_step_mfa_options()
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(reauth_entry, data=self._data)
|
||||
|
||||
utility = select_utility(self._data[CONF_UTILITY])
|
||||
schema_dict: VolDictType = {
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
if select_utility(reauth_entry.data[CONF_UTILITY]).accepts_mfa():
|
||||
schema[vol.Optional(CONF_TOTP_SECRET)] = str
|
||||
if utility.accepts_totp_secret():
|
||||
schema_dict[vol.Optional(CONF_TOTP_SECRET)] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(schema),
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(schema_dict), self._data
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={CONF_NAME: reauth_entry.title},
|
||||
)
|
||||
|
@@ -4,3 +4,4 @@ DOMAIN = "opower"
|
||||
|
||||
CONF_UTILITY = "utility"
|
||||
CONF_TOTP_SECRET = "totp_secret"
|
||||
CONF_LOGIN_DATA = "login_data"
|
||||
|
@@ -14,7 +14,7 @@ from opower import (
|
||||
ReadResolution,
|
||||
create_cookie_jar,
|
||||
)
|
||||
from opower.exceptions import ApiException, CannotConnect, InvalidAuth
|
||||
from opower.exceptions import ApiException, CannotConnect, InvalidAuth, MfaChallenge
|
||||
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.components.recorder.models import (
|
||||
@@ -36,7 +36,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN
|
||||
from .const import CONF_LOGIN_DATA, CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -69,6 +69,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
config_entry.data[CONF_USERNAME],
|
||||
config_entry.data[CONF_PASSWORD],
|
||||
config_entry.data.get(CONF_TOTP_SECRET),
|
||||
config_entry.data.get(CONF_LOGIN_DATA),
|
||||
)
|
||||
|
||||
@callback
|
||||
@@ -90,7 +91,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
# Given the infrequent updating (every 12h)
|
||||
# assume previous session has expired and re-login.
|
||||
await self.api.async_login()
|
||||
except InvalidAuth as err:
|
||||
except (InvalidAuth, MfaChallenge) as err:
|
||||
_LOGGER.error("Error during login: %s", err)
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except CannotConnect as err:
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.12.4"]
|
||||
"requirements": ["opower==0.15.1"]
|
||||
}
|
||||
|
@@ -3,27 +3,43 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"utility": "Utility name",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"utility": "Utility name"
|
||||
},
|
||||
"data_description": {
|
||||
"utility": "The name of your utility provider",
|
||||
"username": "The username for your utility account",
|
||||
"password": "The password for your utility account"
|
||||
"utility": "The name of your utility provider"
|
||||
}
|
||||
},
|
||||
"mfa": {
|
||||
"description": "The TOTP secret below is not one of the 6-digit time-based numeric codes. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation.",
|
||||
"credentials": {
|
||||
"title": "Enter Credentials",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"totp_secret": "TOTP secret"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::opower::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::opower::config::step::user::data_description::password%]",
|
||||
"totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)."
|
||||
"username": "The username for your utility account",
|
||||
"password": "The password for your utility account",
|
||||
"totp_secret": "This is not a 6-digit code. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation."
|
||||
}
|
||||
},
|
||||
"mfa_options": {
|
||||
"title": "Multi-factor authentication",
|
||||
"description": "Your account requires multi-factor authentication (MFA). Select a method to receive your security code.",
|
||||
"data": {
|
||||
"mfa_method": "MFA method"
|
||||
},
|
||||
"data_description": {
|
||||
"mfa_method": "How to receive your security code"
|
||||
}
|
||||
},
|
||||
"mfa_code": {
|
||||
"title": "Enter security code",
|
||||
"description": "A security code has been sent via your selected method. Please enter it below to complete login.",
|
||||
"data": {
|
||||
"mfa_code": "Security code"
|
||||
},
|
||||
"data_description": {
|
||||
"mfa_code": "Typically a 6-digit code"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
@@ -31,18 +47,19 @@
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"totp_secret": "[%key:component::opower::config::step::mfa::data::totp_secret%]"
|
||||
"totp_secret": "[%key:component::opower::config::step::credentials::data::totp_secret%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::opower::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::opower::config::step::user::data_description::password%]",
|
||||
"totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)."
|
||||
"username": "[%key:component::opower::config::step::credentials::data_description::username%]",
|
||||
"password": "[%key:component::opower::config::step::credentials::data_description::password%]",
|
||||
"totp_secret": "[%key:component::opower::config::step::credentials::data_description::totp_secret%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_mfa_code": "The security code is incorrect. Please try again."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
|
@@ -6,7 +6,7 @@ from abc import abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from psnawp_api.core.psnawp_exceptions import (
|
||||
PSNAWPAuthenticationError,
|
||||
@@ -29,7 +29,7 @@ from homeassistant.exceptions import (
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_ACCOUNT_ID, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .helpers import PlaystationNetwork, PlaystationNetworkData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -176,7 +176,9 @@ class PlaystationNetworkFriendDataCoordinator(
|
||||
|
||||
def _setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
self.user = self.psn.psn.user(account_id=self.subentry.data[CONF_ACCOUNT_ID])
|
||||
if TYPE_CHECKING:
|
||||
assert self.subentry.unique_id
|
||||
self.user = self.psn.psn.user(account_id=self.subentry.unique_id)
|
||||
self.profile = self.user.profile()
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
|
@@ -82,6 +82,7 @@
|
||||
},
|
||||
"sensor_device_class": {
|
||||
"options": {
|
||||
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
|
||||
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
||||
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
||||
"area": "[%key:component::sensor::entity_component::area::name%]",
|
||||
|
@@ -39,6 +39,7 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]):
|
||||
_LOGGER,
|
||||
name=f"{DOMAIN}_{config_entry.title}",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
always_update=True,
|
||||
)
|
||||
self._client = get_async_client(hass)
|
||||
|
@@ -59,7 +59,7 @@ PLATFORMS = [
|
||||
Platform.UPDATE,
|
||||
]
|
||||
DEVICE_UPDATE_INTERVAL = timedelta(seconds=60)
|
||||
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=12)
|
||||
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=24)
|
||||
NUM_CRED_ERRORS = 3
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
@@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics(
|
||||
IPC_cam[ch]["hardware version"] = api.camera_hardware_version(ch)
|
||||
IPC_cam[ch]["firmware version"] = api.camera_sw_version(ch)
|
||||
IPC_cam[ch]["encoding main"] = await api.get_encoding(ch)
|
||||
if (signal := api.wifi_signal(ch)) is not None:
|
||||
if (signal := api.wifi_signal(ch)) is not None and api.wifi_connection(ch):
|
||||
IPC_cam[ch]["WiFi signal"] = signal
|
||||
|
||||
chimes: dict[int, dict[str, Any]] = {}
|
||||
@@ -43,7 +43,7 @@ async def async_get_config_entry_diagnostics(
|
||||
"HTTP(S) port": api.port,
|
||||
"Baichuan port": api.baichuan.port,
|
||||
"Baichuan only": api.baichuan_only,
|
||||
"WiFi connection": api.wifi_connection,
|
||||
"WiFi connection": api.wifi_connection(),
|
||||
"WiFi signal": api.wifi_signal(),
|
||||
"RTMP enabled": api.rtmp_enabled,
|
||||
"RTSP enabled": api.rtsp_enabled,
|
||||
|
@@ -19,5 +19,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.14.4"]
|
||||
"requirements": ["reolink-aio==0.14.6"]
|
||||
}
|
||||
|
@@ -148,7 +148,7 @@ HOST_SENSORS = (
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
entity_registry_enabled_default=False,
|
||||
value=lambda api: api.wifi_signal(),
|
||||
supported=lambda api: api.supported(None, "wifi") and api.wifi_connection,
|
||||
supported=lambda api: api.supported(None, "wifi") and api.wifi_connection(),
|
||||
),
|
||||
ReolinkHostSensorEntityDescription(
|
||||
key="cpu_usage",
|
||||
|
@@ -573,6 +573,7 @@ class SimpliSafe:
|
||||
self._hass,
|
||||
LOGGER,
|
||||
name=self.entry.title,
|
||||
config_entry=self.entry,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
update_method=self.async_update,
|
||||
)
|
||||
|
@@ -74,6 +74,7 @@ class SmartTubController:
|
||||
self._hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
config_entry=entry,
|
||||
update_method=self.async_update_data,
|
||||
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
||||
)
|
||||
|
@@ -83,8 +83,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if not gateway:
|
||||
raise ConfigEntryNotReady(f"Cannot find device {device}")
|
||||
|
||||
signal_coordinator = SignalCoordinator(hass, gateway)
|
||||
network_coordinator = NetworkCoordinator(hass, gateway)
|
||||
signal_coordinator = SignalCoordinator(hass, entry, gateway)
|
||||
network_coordinator = NetworkCoordinator(hass, entry, gateway)
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await signal_coordinator.async_config_entry_first_refresh()
|
||||
|
@@ -16,13 +16,14 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class SignalCoordinator(DataUpdateCoordinator):
|
||||
"""Signal strength coordinator."""
|
||||
|
||||
def __init__(self, hass, gateway):
|
||||
def __init__(self, hass, entry, gateway):
|
||||
"""Initialize signal strength coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="Device signal state",
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
config_entry=entry,
|
||||
)
|
||||
self._gateway = gateway
|
||||
|
||||
@@ -38,13 +39,14 @@ class SignalCoordinator(DataUpdateCoordinator):
|
||||
class NetworkCoordinator(DataUpdateCoordinator):
|
||||
"""Network info coordinator."""
|
||||
|
||||
def __init__(self, hass, gateway):
|
||||
def __init__(self, hass, entry, gateway):
|
||||
"""Initialize network info coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="Device network state",
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
config_entry=entry,
|
||||
)
|
||||
self._gateway = gateway
|
||||
|
||||
|
@@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SnooConfigEntry) -> bool
|
||||
coordinators: dict[str, SnooCoordinator] = {}
|
||||
tasks = []
|
||||
for device in devices:
|
||||
coordinators[device.serialNumber] = SnooCoordinator(hass, device, snoo)
|
||||
coordinators[device.serialNumber] = SnooCoordinator(hass, entry, device, snoo)
|
||||
tasks.append(coordinators[device.serialNumber].setup())
|
||||
await asyncio.gather(*tasks)
|
||||
entry.runtime_data = coordinators
|
||||
|
@@ -19,11 +19,18 @@ class SnooCoordinator(DataUpdateCoordinator[SnooData]):
|
||||
|
||||
config_entry: SnooConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, device: SnooDevice, snoo: Snoo) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: SnooConfigEntry,
|
||||
device: SnooDevice,
|
||||
snoo: Snoo,
|
||||
) -> None:
|
||||
"""Set up Snoo Coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
name=device.name,
|
||||
config_entry=entry,
|
||||
logger=_LOGGER,
|
||||
)
|
||||
self.device_unique_id = device.serialNumber
|
||||
|
@@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["soco", "sonos_websocket"],
|
||||
"requirements": ["soco==0.30.9", "sonos-websocket==0.1.3"],
|
||||
"requirements": ["soco==0.30.11", "sonos-websocket==0.1.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
|
||||
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pysqueezebox import Player
|
||||
@@ -21,6 +22,8 @@ from homeassistant.helpers.network import is_internal_request
|
||||
|
||||
from .const import DOMAIN, UNPLAYABLE_TYPES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LIBRARY = [
|
||||
"favorites",
|
||||
"artists",
|
||||
@@ -138,18 +141,42 @@ class BrowseData:
|
||||
self.squeezebox_id_by_type.update(SQUEEZEBOX_ID_BY_TYPE)
|
||||
self.media_type_to_squeezebox.update(MEDIA_TYPE_TO_SQUEEZEBOX)
|
||||
|
||||
def add_new_command(self, cmd: str | MediaType, type: str) -> None:
|
||||
"""Add items to maps for new apps or radios."""
|
||||
self.known_apps_radios.add(cmd)
|
||||
self.media_type_to_squeezebox[cmd] = cmd
|
||||
self.squeezebox_id_by_type[cmd] = type
|
||||
self.content_type_media_class[cmd] = {
|
||||
"item": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.TRACK,
|
||||
}
|
||||
self.content_type_to_child_type[cmd] = MediaType.TRACK
|
||||
|
||||
def _add_new_command_to_browse_data(
|
||||
browse_data: BrowseData, cmd: str | MediaType, type: str
|
||||
) -> None:
|
||||
"""Add items to maps for new apps or radios."""
|
||||
browse_data.media_type_to_squeezebox[cmd] = cmd
|
||||
browse_data.squeezebox_id_by_type[cmd] = type
|
||||
browse_data.content_type_media_class[cmd] = {
|
||||
"item": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.TRACK,
|
||||
}
|
||||
browse_data.content_type_to_child_type[cmd] = MediaType.TRACK
|
||||
async def async_init(self, player: Player, browse_limit: int) -> None:
|
||||
"""Initialize known apps and radios from the player."""
|
||||
|
||||
cmd = ["apps", 0, browse_limit]
|
||||
result = await player.async_query(*cmd)
|
||||
for app in result["appss_loop"]:
|
||||
app_cmd = "app-" + app["cmd"]
|
||||
if app_cmd not in self.known_apps_radios:
|
||||
self.add_new_command(app_cmd, "item_id")
|
||||
_LOGGER.debug(
|
||||
"Adding new command %s to browse data for player %s",
|
||||
app_cmd,
|
||||
player.player_id,
|
||||
)
|
||||
cmd = ["radios", 0, browse_limit]
|
||||
result = await player.async_query(*cmd)
|
||||
for app in result["radioss_loop"]:
|
||||
app_cmd = "app-" + app["cmd"]
|
||||
if app_cmd not in self.known_apps_radios:
|
||||
self.add_new_command(app_cmd, "item_id")
|
||||
_LOGGER.debug(
|
||||
"Adding new command %s to browse data for player %s",
|
||||
app_cmd,
|
||||
player.player_id,
|
||||
)
|
||||
|
||||
|
||||
def _build_response_apps_radios_category(
|
||||
@@ -292,8 +319,7 @@ async def build_item_response(
|
||||
app_cmd = "app-" + item["cmd"]
|
||||
|
||||
if app_cmd not in browse_data.known_apps_radios:
|
||||
browse_data.known_apps_radios.add(app_cmd)
|
||||
_add_new_command_to_browse_data(browse_data, app_cmd, "item_id")
|
||||
browse_data.add_new_command(app_cmd, "item_id")
|
||||
|
||||
child_media = _build_response_apps_radios_category(
|
||||
browse_data=browse_data, cmd=app_cmd, item=item
|
||||
|
@@ -311,6 +311,11 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
)
|
||||
return None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await self._browse_data.async_init(self._player, self.browse_limit)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove from list of known players when removed from hass."""
|
||||
self.coordinator.config_entry.runtime_data.known_player_ids.remove(
|
||||
|
@@ -41,5 +41,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["switchbot"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["PySwitchbot==0.68.2"]
|
||||
"requirements": ["PySwitchbot==0.68.3"]
|
||||
}
|
||||
|
@@ -216,6 +216,7 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity):
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_extra_optimistic_options = (CONF_POSITION,)
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
|
@@ -20,6 +20,7 @@ class AbstractTemplateEntity(Entity):
|
||||
|
||||
_entity_id_format: str
|
||||
_optimistic_entity: bool = False
|
||||
_extra_optimistic_options: tuple[str, ...] | None = None
|
||||
_template: Template | None = None
|
||||
|
||||
def __init__(
|
||||
@@ -35,9 +36,14 @@ class AbstractTemplateEntity(Entity):
|
||||
if self._optimistic_entity:
|
||||
self._template = config.get(CONF_STATE)
|
||||
|
||||
self._attr_assumed_state = self._template is None or config.get(
|
||||
CONF_OPTIMISTIC, False
|
||||
)
|
||||
optimistic = self._template is None
|
||||
if self._extra_optimistic_options:
|
||||
optimistic = optimistic and all(
|
||||
config.get(option) is None
|
||||
for option in self._extra_optimistic_options
|
||||
)
|
||||
|
||||
self._attr_assumed_state = optimistic or config.get(CONF_OPTIMISTIC, False)
|
||||
|
||||
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
|
||||
self.entity_id = async_generate_entity_id(
|
||||
|
@@ -2,6 +2,7 @@
|
||||
"common": {
|
||||
"advanced_options": "Advanced options",
|
||||
"availability": "Availability template",
|
||||
"availability_description": "Defines a template to get the `available` state of the entity. If the template either fails to render or returns `True`, `\"1\"`, `\"true\"`, `\"yes\"`, `\"on\"`, `\"enable\"`, or a non-zero number, the entity will be `available`. If the template returns any other value, the entity will be `unavailable`. If not configured, the entity will always be `available`. Note that the string comparison is not case sensitive; `\"TrUe\"` and `\"yEs\"` are allowed.",
|
||||
"code_format": "Code format",
|
||||
"device_class": "Device class",
|
||||
"device_id_description": "Select a device to link to this entity.",
|
||||
@@ -28,13 +29,26 @@
|
||||
"code_format": "[%key:component::template::common::code_format%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"value_template": "Defines a template to set the state of the alarm panel. Valid output values from the template are `armed_away`, `armed_home`, `armed_night`, `armed_vacation`, `arming`, `disarmed`, `pending`, and `triggered`.",
|
||||
"disarm": "Defines actions to run when the alarm control panel is disarmed. Receives variable `code`.",
|
||||
"arm_away": "Defines actions to run when the alarm control panel is armed to `arm_away`. Receives variable `code`.",
|
||||
"arm_custom_bypass": "Defines actions to run when the alarm control panel is armed to `arm_custom_bypass`. Receives variable `code`.",
|
||||
"arm_home": "Defines actions to run when the alarm control panel is armed to `arm_home`. Receives variable `code`.",
|
||||
"arm_night": "Defines actions to run when the alarm control panel is armed to `arm_night`. Receives variable `code`.",
|
||||
"arm_vacation": "Defines actions to run when the alarm control panel is armed to `arm_vacation`. Receives variable `code`.",
|
||||
"trigger": "Defines actions to run when the alarm control panel is triggered. Receives variable `code`.",
|
||||
"code_arm_required": "If true, the code is required to arm the alarm.",
|
||||
"code_format": "One of `number`, `text` or `no_code`. Format for the code used to arm/disarm the alarm."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -48,13 +62,17 @@
|
||||
"state": "[%key:component::template::common::state%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "The sensor is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68,13 +86,17 @@
|
||||
"press": "Actions on press"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"press": "Defines actions to run when button is pressed."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -99,13 +121,16 @@
|
||||
"close_cover": "Defines actions to run when the cover is closed.",
|
||||
"stop_cover": "Defines actions to run when the cover is stopped.",
|
||||
"position": "Defines a template to get the position of the cover. Value values are numbers between `0` (`closed`) and `100` (`open`).",
|
||||
"set_cover_position": "Defines actions to run when the cover is given a `set_cover_position` command."
|
||||
"set_cover_position": "Defines actions to run when the cover is given a `set_cover_position` command. Receives variable `position`."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -124,11 +149,11 @@
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "Defines a template to get the state of the fan. Valid values: `on`, `off`.",
|
||||
"state": "The fan is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`.",
|
||||
"turn_off": "Defines actions to run when the fan is turned off.",
|
||||
"turn_on": "Defines actions to run when the fan is turned on.",
|
||||
"turn_on": "Defines actions to run when the fan is turned on. Receives variables `percentage` and/or `preset_mode`.",
|
||||
"percentage": "Defines a template to get the speed percentage of the fan.",
|
||||
"set_percentage": "Defines actions to run when the fan is given a speed percentage command.",
|
||||
"set_percentage": "Defines actions to run when the fan is given a speed percentage command. Receives variable `percentage`.",
|
||||
"speed_count": "The number of speeds the fan supports. Used to calculate the percentage step for the `fan.increase_speed` and `fan.decrease_speed` actions."
|
||||
},
|
||||
"sections": {
|
||||
@@ -136,6 +161,9 @@
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -149,13 +177,18 @@
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"url": "Defines a template to get the URL on which the image is served.",
|
||||
"verify_ssl": "Enable or disable SSL certificate verification. Disable to use an http URL, or if you have a self-signed SSL certificate and haven’t installed the CA certificate to enable verification."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -176,13 +209,25 @@
|
||||
"set_temperature": "Actions on set color temperature"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "The light is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`.",
|
||||
"turn_off": "Defines actions to run when the light is turned off.",
|
||||
"turn_on": "Defines actions to run when the light is turned on.",
|
||||
"level": "Defines a template to get the brightness of the light. Valid values are 0 to 255.",
|
||||
"set_level": "Defines actions to run when the light is given a brightness command. The script will only be called if the `turn_on` call only has `brightness`, and optionally `transition`. Receives variables `brightness` and, optionally, `transition`.",
|
||||
"hs": "Defines a template to get the HS color of the light. Must render a tuple (hue, saturation).",
|
||||
"set_hs": "Defines actions to run when the light is given a hs color command. Available variables: `hs` as a tuple, `h` and `s`.",
|
||||
"temperature": "Defines a template to get the color temperature of the light.",
|
||||
"set_temperature": "Defines actions to run when the light is given a color temperature command. Receives variable `color_temp_kelvin`. May also receive variables `brightness` and/or `transition`."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -199,13 +244,21 @@
|
||||
"open": "Actions on open"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "Defines a template to set the state of the lock. The lock is locked if the template evaluates to `True`, `true`, `on`, or `locked`. The lock is unlocked if the template evaluates to `False`, `false`, `off`, or `unlocked`. Other valid states are `jammed`, `opening`, `locking`, `open`, and `unlocking`.",
|
||||
"lock": "Defines actions to run when the lock is locked.",
|
||||
"unlock": "Defines actions to run when the lock is unlocked.",
|
||||
"code_format": "Defines a template to get the `code_format` attribute of the lock.",
|
||||
"open": "Defines actions to run when the lock is opened."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -223,13 +276,22 @@
|
||||
"unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "Template for the number's current value.",
|
||||
"step": "Defines the number's increment/decrement step.",
|
||||
"set_value": "Defines actions to run when the number is set to a value. Receives variable `value`.",
|
||||
"max": "Defines the number's maximum value.",
|
||||
"min": "Defines the number's minimum value.",
|
||||
"unit_of_measurement": "Defines the unit of measurement of the number, if any."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -244,13 +306,19 @@
|
||||
"options": "Available options"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "Template for the select’s current value.",
|
||||
"select_option": "Defines actions to run when an `option` from the `options` list is selected. Receives variable `option`.",
|
||||
"options": "Template for the select’s available options."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -266,13 +334,18 @@
|
||||
"unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "Defines a template to get the state of the sensor. If the sensor is numeric, i.e. it has a `state_class` or a `unit_of_measurement`, the state template must render to a number or to `none`. The state template must not render to a string, including `unknown` or `unavailable`. An `availability` template may be defined to suppress rendering of the state template.",
|
||||
"unit_of_measurement": "Defines the unit of measurement for the sensor, if any. This will also display the value based on the number format setting in the user profile and influence the graphical presentation in the history visualization as a continuous value."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -307,13 +380,18 @@
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful."
|
||||
"value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful.",
|
||||
"turn_off": "Defines actions to run when the switch is turned off.",
|
||||
"turn_on": "Defines actions to run when the switch is turned on."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -324,24 +402,37 @@
|
||||
"device_id": "[%key:common::config_flow::data::device%]",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"state": "[%key:component::template::common::state%]",
|
||||
"start": "Actions on turn off",
|
||||
"start": "Actions on start",
|
||||
"fan_speed": "Fan speed",
|
||||
"fan_speeds": "Fan speeds",
|
||||
"set_fan_speed": "Actions on set fan speed",
|
||||
"stop": "Actions on stop",
|
||||
"pause": "Actions on pause",
|
||||
"return_to_base": "Actions on return to base",
|
||||
"return_to_base": "Actions on return to dock",
|
||||
"clean_spot": "Actions on clean spot",
|
||||
"locate": "Actions on locate"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "Defines a template to get the state of the vacuum. Valid values are `cleaning`, `docked`, `idle`, `paused`, `returning`, and `error`.",
|
||||
"start": "Defines actions to run when the vacuum is started.",
|
||||
"fan_speed": "Defines a template to get the fan speed of the vacuum.",
|
||||
"fan_speeds": "List of fan speeds supported by the vacuum.",
|
||||
"set_fan_speed": "Defines actions to run when the vacuum is given a command to set the fan speed. Receives variable `fan_speed`.",
|
||||
"stop": "Defines actions to run when the vacuum is stopped.",
|
||||
"pause": "Defines actions to run when the vacuum is paused.",
|
||||
"return_to_base": "Defines actions to run when the vacuum is given a 'Return to dock' command.",
|
||||
"clean_spot": "Defines actions to run when the vacuum is given a 'Clean spot' command.",
|
||||
"locate": "Defines actions to run when the vacuum is given a 'Locate' command."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -349,6 +440,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_battery_level": {
|
||||
"title": "Deprecated battery level option in {entity_name}",
|
||||
"description": "The template vacuum options `battery_level` and `battery_level_template` are being removed in 2026.8.\n\nPlease remove the `battery_level` or `battery_level_template` option from the YAML configuration for {entity_id} ({entity_name})."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"alarm_control_panel": {
|
||||
@@ -366,13 +463,26 @@
|
||||
"code_format": "[%key:component::template::common::code_format%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"value_template": "[%key:component::template::config::step::alarm_control_panel::data_description::value_template%]",
|
||||
"disarm": "[%key:component::template::config::step::alarm_control_panel::data_description::disarm%]",
|
||||
"arm_away": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_away%]",
|
||||
"arm_custom_bypass": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_custom_bypass%]",
|
||||
"arm_home": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_home%]",
|
||||
"arm_night": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_night%]",
|
||||
"arm_vacation": "[%key:component::template::config::step::alarm_control_panel::data_description::arm_vacation%]",
|
||||
"trigger": "[%key:component::template::config::step::alarm_control_panel::data_description::trigger%]",
|
||||
"code_arm_required": "[%key:component::template::config::step::alarm_control_panel::data_description::code_arm_required%]",
|
||||
"code_format": "[%key:component::template::config::step::alarm_control_panel::data_description::code_format%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -384,13 +494,17 @@
|
||||
"state": "[%key:component::template::common::state%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "[%key:component::template::config::step::binary_sensor::data_description::state%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -402,13 +516,17 @@
|
||||
"press": "[%key:component::template::config::step::button::data::press%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"press": "[%key:component::template::config::step::button::data_description::press%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -439,6 +557,9 @@
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -468,6 +589,9 @@
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -480,13 +604,18 @@
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"url": "[%key:component::template::config::step::image::data_description::url%]",
|
||||
"verify_ssl": "[%key:component::template::config::step::image::data_description::verify_ssl%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -507,13 +636,25 @@
|
||||
"set_temperature": "[%key:component::template::config::step::light::data::set_temperature%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "[%key:component::template::config::step::light::data_description::state%]",
|
||||
"turn_off": "[%key:component::template::config::step::light::data_description::turn_off%]",
|
||||
"turn_on": "[%key:component::template::config::step::light::data_description::turn_on%]",
|
||||
"level": "[%key:component::template::config::step::light::data_description::level%]",
|
||||
"set_level": "[%key:component::template::config::step::light::data_description::set_level%]",
|
||||
"hs": "[%key:component::template::config::step::light::data_description::hs%]",
|
||||
"set_hs": "[%key:component::template::config::step::light::data_description::set_hs%]",
|
||||
"temperature": "[%key:component::template::config::step::light::data_description::temperature%]",
|
||||
"set_temperature": "[%key:component::template::config::step::light::data_description::set_temperature%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -529,13 +670,21 @@
|
||||
"open": "[%key:component::template::config::step::lock::data::open%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "[%key:component::template::config::step::lock::data_description::state%]",
|
||||
"lock": "[%key:component::template::config::step::lock::data_description::lock%]",
|
||||
"unlock": "[%key:component::template::config::step::lock::data_description::unlock%]",
|
||||
"code_format": "[%key:component::template::config::step::lock::data_description::code_format%]",
|
||||
"open": "[%key:component::template::config::step::lock::data_description::open%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -552,13 +701,21 @@
|
||||
"min": "[%key:component::template::config::step::number::data::min%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "[%key:component::template::config::step::number::data_description::state%]",
|
||||
"step": "[%key:component::template::config::step::number::data_description::step%]",
|
||||
"set_value": "[%key:component::template::config::step::number::data_description::set_value%]",
|
||||
"max": "[%key:component::template::config::step::number::data_description::max%]",
|
||||
"min": "[%key:component::template::config::step::number::data_description::min%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -573,13 +730,19 @@
|
||||
"options": "[%key:component::template::config::step::select::data::options%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "[%key:component::template::config::step::select::data_description::state%]",
|
||||
"select_option": "[%key:component::template::config::step::select::data_description::select_option%]",
|
||||
"options": "[%key:component::template::config::step::select::data_description::options%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -594,13 +757,18 @@
|
||||
"unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "[%key:component::template::config::step::sensor::data_description::state%]",
|
||||
"unit_of_measurement": "[%key:component::template::config::step::sensor::data_description::unit_of_measurement%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -616,13 +784,18 @@
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"value_template": "[%key:component::template::config::step::switch::data_description::value_template%]"
|
||||
"value_template": "[%key:component::template::config::step::switch::data_description::value_template%]",
|
||||
"turn_off": "[%key:component::template::config::step::switch::data_description::turn_off%]",
|
||||
"turn_on": "[%key:component::template::config::step::switch::data_description::turn_on%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -644,17 +817,30 @@
|
||||
"locate": "[%key:component::template::config::step::vacuum::data::locate%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "[%key:component::template::config::step::vacuum::data_description::state%]",
|
||||
"start": "[%key:component::template::config::step::vacuum::data_description::start%]",
|
||||
"fan_speed": "[%key:component::template::config::step::vacuum::data_description::fan_speed%]",
|
||||
"fan_speeds": "[%key:component::template::config::step::vacuum::data_description::fan_speeds%]",
|
||||
"set_fan_speed": "[%key:component::template::config::step::vacuum::data_description::set_fan_speed%]",
|
||||
"stop": "[%key:component::template::config::step::vacuum::data_description::stop%]",
|
||||
"pause": "[%key:component::template::config::step::vacuum::data_description::pause%]",
|
||||
"return_to_base": "[%key:component::template::config::step::vacuum::data_description::return_to_base%]",
|
||||
"clean_spot": "[%key:component::template::config::step::vacuum::data_description::clean_spot%]",
|
||||
"locate": "[%key:component::template::config::step::vacuum::data_description::locate%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Template vacuum"
|
||||
"title": "[%key:component::template::config::step::vacuum::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -721,6 +907,7 @@
|
||||
},
|
||||
"sensor_device_class": {
|
||||
"options": {
|
||||
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
|
||||
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
||||
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
||||
"area": "[%key:component::sensor::entity_component::area::name%]",
|
||||
@@ -768,7 +955,7 @@
|
||||
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
|
||||
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
|
||||
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
||||
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
||||
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]",
|
||||
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
|
||||
"volume": "[%key:component::sensor::entity_component::volume::name%]",
|
||||
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
|
||||
|
@@ -34,11 +34,16 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
issue_registry as ir,
|
||||
template,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -188,6 +193,26 @@ def async_create_preview_vacuum(
|
||||
)
|
||||
|
||||
|
||||
def create_issue(
|
||||
hass: HomeAssistant, supported_features: int, name: str, entity_id: str
|
||||
) -> None:
|
||||
"""Create the battery_level issue."""
|
||||
if supported_features & VacuumEntityFeature.BATTERY:
|
||||
key = "deprecated_battery_level"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"{key}_{entity_id}",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=key,
|
||||
translation_placeholders={
|
||||
"entity_name": name,
|
||||
"entity_id": entity_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
|
||||
"""Representation of a template vacuum features."""
|
||||
|
||||
@@ -369,6 +394,16 @@ class TemplateStateVacuumEntity(TemplateEntity, AbstractTemplateVacuum):
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
self._attr_supported_features |= supported_feature
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
create_issue(
|
||||
self.hass,
|
||||
self._attr_supported_features,
|
||||
self._attr_name or DEFAULT_NAME,
|
||||
self.entity_id,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_setup_templates(self) -> None:
|
||||
"""Set up templates."""
|
||||
@@ -434,6 +469,16 @@ class TriggerVacuumEntity(TriggerEntity, AbstractTemplateVacuum):
|
||||
self._to_render_simple.append(key)
|
||||
self._parse_result.add(key)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
create_issue(
|
||||
self.hass,
|
||||
self._attr_supported_features,
|
||||
self._attr_name or DEFAULT_NAME,
|
||||
self.entity_id,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle update of the data."""
|
||||
|
@@ -34,6 +34,7 @@ from homeassistant.components.weather import (
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_TEMPERATURE_UNIT,
|
||||
CONF_UNIQUE_ID,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
@@ -151,6 +152,7 @@ PLATFORM_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS),
|
||||
vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS),
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS),
|
||||
vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template,
|
||||
|
@@ -28,7 +28,7 @@ class TeslemetryData:
|
||||
vehicles: list[TeslemetryVehicleData]
|
||||
energysites: list[TeslemetryEnergyData]
|
||||
scopes: list[Scope]
|
||||
stream: TeslemetryStream
|
||||
stream: TeslemetryStream | None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@@ -45,7 +45,7 @@ from .entity import (
|
||||
TeslemetryVehicleStreamEntity,
|
||||
TeslemetryWallConnectorEntity,
|
||||
)
|
||||
from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData
|
||||
from .models import TeslemetryEnergyData, TeslemetryVehicleData
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -1617,11 +1617,12 @@ async def async_setup_entry(
|
||||
if energysite.history_coordinator is not None
|
||||
)
|
||||
|
||||
entities.append(
|
||||
TeslemetryCreditBalanceSensor(
|
||||
entry.unique_id or entry.entry_id, entry.runtime_data
|
||||
if entry.runtime_data.stream is not None:
|
||||
entities.append(
|
||||
TeslemetryCreditBalanceSensor(
|
||||
entry.unique_id or entry.entry_id, entry.runtime_data.stream
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -1840,12 +1841,12 @@ class TeslemetryCreditBalanceSensor(RestoreSensor):
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_suggested_display_precision = 0
|
||||
|
||||
def __init__(self, uid: str, data: TeslemetryData) -> None:
|
||||
def __init__(self, uid: str, stream: TeslemetryStream) -> None:
|
||||
"""Initialize common aspects of a Teslemetry entity."""
|
||||
|
||||
self._attr_translation_key = "credit_balance"
|
||||
self._attr_unique_id = f"{uid}_credit_balance"
|
||||
self.stream = data.stream
|
||||
self.stream = stream
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
|
@@ -153,6 +153,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool
|
||||
# Register known device IDs
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in manager.device_map.values():
|
||||
LOGGER.debug(
|
||||
"Register device %s: %s (function: %s, status range: %s)",
|
||||
device.id,
|
||||
device.status,
|
||||
device.function,
|
||||
device.status_range,
|
||||
)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, device.id)},
|
||||
@@ -237,6 +244,14 @@ class DeviceListener(SharingDeviceListener):
|
||||
# Ensure the device isn't present stale
|
||||
self.hass.add_job(self.async_remove_device, device.id)
|
||||
|
||||
LOGGER.debug(
|
||||
"Add device %s: %s (function: %s, status range: %s)",
|
||||
device.id,
|
||||
device.status,
|
||||
device.function,
|
||||
device.status_range,
|
||||
)
|
||||
|
||||
dispatcher_send(self.hass, TUYA_DISCOVERY_NEW, [device.id])
|
||||
|
||||
def remove_device(self, device_id: str) -> None:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user