Compare commits

...

78 Commits

Author SHA1 Message Date
abmantis
d6caa86ab3 Add support for trigger descriptions with relative keys 2025-08-03 16:25:36 +01:00
Andrew Jackson
4318e29ce8 Bump aiomealie to 0.10.1 (#149890) 2025-08-03 14:18:13 +02:00
Martin Hjelmare
fea5c63bba Fix Z-Wave handling of driver ready event (#149879) 2025-08-03 11:23:01 +02:00
Åke Strandberg
b2349ac2bd Improve miele climate test coverage (#149859) 2025-08-03 11:19:08 +02:00
Marc Mueller
08f7b708a4 Update pytest warnings filter (#149839) 2025-08-03 09:25:17 +02:00
Martin Hjelmare
1236801b7d Fix Z-Wave config entry state conditions in listen task (#149841) 2025-08-02 23:07:16 +02:00
Thomas D
72d9dbf39d Add scopes in config flow auth request for Volvo integration (#149813) 2025-08-02 22:17:13 +02:00
Thomas D
755864f9f3 Add sensor platform to Qbus integration (#149389)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-08-02 20:01:58 +02:00
peteS-UK
fa476d4e34 Fix initialisation of Apps and Radios list for Squeezebox (#149834) 2025-08-02 20:01:02 +02:00
Manu
018197e41a Add notifiers to send direct messages to friends in PlayStation Network (#149844) 2025-08-02 19:55:45 +02:00
Brett Adams
7dd2b9e422 Make history coordinator more reliable in Tesla Fleet (#149854) 2025-08-02 19:54:19 +02:00
hahn-th
3e615fd373 Improve code quality for garage door modules in homematicip_cloud (#149856) 2025-08-02 19:51:08 +02:00
Oliver
c0bf167e10 Update denonavr to 1.1.2 (#149842) 2025-08-02 19:44:01 +02:00
Andrea Turri
45f6778ff4 Fix Miele hob translation keys (#149865) 2025-08-02 18:37:57 +02:00
Jamin
bddd4d621a Bump VoIP utils to 0.3.4 (#149786) 2025-08-01 20:37:45 +01:00
Norbert Rittel
b0e75e9ee4 Update reference for volatile_organic_compounds_parts in template (#149831) 2025-08-01 20:36:10 +01:00
Norbert Rittel
d45c03a795 Update reference for volatile_organic_compounds_parts in random (#149832) 2025-08-01 20:35:04 +01:00
Norbert Rittel
8562c8d32f Add translations for recently introduced device classes to scrape (#149822) 2025-08-01 20:34:31 +01:00
Norbert Rittel
ae42d71123 Add translations for recently introduced device classes to sql (#149821) 2025-08-01 20:33:47 +01:00
Alexandre CUER
9616c8cd7b Bump pyemoncms to 0.1.2 (#149825) 2025-08-01 20:04:16 +01:00
kizovinh
9394546668 Add EZVIZ battery camera power status and online status sensor (#146822) 2025-08-01 20:00:53 +01:00
Norbert Rittel
d43f21c2e2 Fix descriptions for template number fields (#149804) 2025-08-01 20:35:48 +02:00
Norbert Rittel
8d68fee9f8 Add translation for absolute_humidity device class to template (#149814) 2025-08-01 18:30:59 +01:00
Willem-Jan van Rootselaar
b4a4e218ec Add re-authentication to BSBLan (#146280)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-08-01 16:42:59 +02:00
Norbert Rittel
fb2d62d692 Add translation for absolute_humidity device class to mqtt (#149818) 2025-08-01 15:57:47 +02:00
Erik Montnemery
f538807d6e Make device suggested_area only influence new devices (#149758)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-08-01 14:54:58 +02:00
Joost Lekkerkerker
a08c3c9f44 Improve Tado binary sensor tests (#149807) 2025-08-01 14:38:12 +02:00
Joost Lekkerkerker
506431c75f Improve Tado water heater tests (#149806) 2025-08-01 14:38:02 +02:00
Joost Lekkerkerker
37579440e6 Improve Tado climate tests (#149808) 2025-08-01 14:37:12 +02:00
Joost Lekkerkerker
5ce2729dc2 Improve Tado sensor tests (#149809) 2025-08-01 14:36:57 +02:00
Joost Lekkerkerker
b5e4ae4a53 Improve Tado switch tests (#149810)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-01 14:36:37 +02:00
Norbert Rittel
3d4386ea6d Add translation for absolute_humidity device class to random (#149815) 2025-08-01 14:32:14 +02:00
Alexandre CUER
9f1cec893e emoncms - fix missing data descriptions (#149733) 2025-08-01 13:22:46 +02:00
starkillerOG
bc87140a6f Update after Motion Blinds tilt change (#149779) 2025-08-01 11:15:49 +02:00
Erik Montnemery
d77a3fca83 Exclude is_new from DeviceEntry snapshots (#149801) 2025-08-01 11:01:26 +02:00
Joakim Sørensen
924a86dfb6 Add nameservers to supervisor system health response (#149749) 2025-08-01 10:51:48 +02:00
Erik Montnemery
0d7608f7c5 Deprecate DeviceEntry.suggested_area (#149730) 2025-08-01 10:34:34 +02:00
Tom
22e054f4cd Add diagnostics to UISP AirOS (#149631) 2025-08-01 09:24:22 +02:00
epenet
8b53b26333 Fix tuya light supported color modes (#149793)
Co-authored-by: Erik <erik@montnemery.com>
2025-08-01 09:13:53 +02:00
Erik Montnemery
4d59e8cd80 Fix flaky velbus test (#149743) 2025-08-01 07:49:51 +02:00
Fabian Leutgeb
61396d92a5 Homekit valve duration characteristics (#149698)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-31 15:21:48 -10:00
Philippe Lafoucrière
c72c600de4 Fix bootstrap script path resolution (#149721) 2025-07-31 23:47:25 +01:00
J. Nick Koston
b86b0c10bd Bump aioesphomeapi to 37.2.2 (#149755) 2025-07-31 12:23:24 -10:00
starkillerOG
eb222f6c5d Bump motionblinds to 0.6.30 (#149764) 2025-08-01 01:09:20 +03:00
Manu
4b5fe424ed Hide configuration URL when Uptime Kuma is installed locally (#149781) 2025-08-01 01:07:56 +03:00
Nathan Spencer
61ca42e923 Bump pylitterbot to 2024.2.3 (#149763) 2025-07-31 21:04:23 +02:00
Copilot
21c1427abf Fix ZHA ContextVar deprecation by passing config_entry (#149748)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joostlek <7083755+joostlek@users.noreply.github.com>
Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
Co-authored-by: TheJulianJES <6409465+TheJulianJES@users.noreply.github.com>
2025-07-31 14:52:17 -04:00
karwosts
aa6b37bc7c Fix add_suggested_values_to_schema when the schema has sections (#149718)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-07-31 20:50:26 +02:00
Marc Mueller
bbc1466cfc Update rpds-py to 0.26.0 (#149753) 2025-07-31 17:51:10 +01:00
Bram Kragten
21a9799060 Update frontend to 20250731.0 (#149757) 2025-07-31 18:46:10 +02:00
Erik Montnemery
f7d54b46ec Improve test of FlowHandler.add_suggested_values_to_schema (#149759) 2025-07-31 17:55:15 +02:00
Erik Montnemery
6ad1b8dcb1 Fix kitchen_sink option flow (#149760) 2025-07-31 17:49:09 +02:00
Abílio Costa
5f6b1212a3 Remove data flow step_id deprecation note (#149714) 2025-07-31 16:04:09 +02:00
dependabot[bot]
58dc6a952e Bump home-assistant/wheels from 2025.03.0 to 2025.07.0 (#149741) 2025-07-31 15:35:55 +02:00
Petro31
59d8df142d Nitpick default translations for template integration (#149740) 2025-07-31 15:19:43 +02:00
Petro31
04fb86b4ba Fix unique_id in config validation for legacy weather platform (#149742) 2025-07-31 15:19:37 +02:00
Erik Montnemery
3d744f032f Make _EventDeviceRegistryUpdatedData_Remove JSON serializable (#149734) 2025-07-31 12:35:13 +02:00
J. Nick Koston
f7c8cdb3a7 Bump aioesphomeapi to 37.2.0 (#149732) 2025-07-31 12:10:23 +02:00
Copilot
3952544822 Fix ContextVar deprecation warning in homeassistant_hardware integration (#149687)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joostlek <7083755+joostlek@users.noreply.github.com>
Co-authored-by: mib1185 <35783820+mib1185@users.noreply.github.com>
2025-07-31 12:06:04 +02:00
Erik Montnemery
42101dd432 Remove result from FlowResult (#149202) 2025-07-31 10:58:36 +02:00
L.
f7eacaa48d Bump xiaomi-ble to 1.2.0 (#149711) 2025-07-31 09:01:06 +02:00
johanzander
ad0db5c83a Update growattServer to version 1.7.1 (#149716) 2025-07-31 08:17:33 +02:00
J. Nick Koston
63216b77c2 Bump aioesphomeapi to 37.1.6 (#149715) 2025-07-30 13:54:18 -10:00
Åke Strandberg
7a55373b0b Fix bug when interpreting miele action response (#149710) 2025-07-31 01:07:12 +02:00
J. Nick Koston
f9e7459901 Fix ESPHome unnecessary probing on DHCP discovery (#149713) 2025-07-31 01:06:08 +02:00
starkillerOG
94dc2e2ea3 Bump reolink-aio to 0.14.5 (#149700) 2025-07-30 22:54:32 +01:00
Åke Strandberg
2cf144fb25 Add missing translations for miele dishwasher (#149702) 2025-07-30 22:45:05 +01:00
Jan Bouwhuis
f318766021 Fix inconsistent use of the term 'target' and a typo in MQTT translation strings (#149703) 2025-07-30 22:42:53 +01:00
Andrea Turri
ec7fb140ac Fix Miele induction hob empty state (#149706) 2025-07-30 22:38:11 +01:00
Petro31
2706c7d67d Add translations for all fields in template integration (#149692)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-07-30 22:30:05 +01:00
Roman Sivriver
b4e50902eb Fix typo in backup log message (#149705) 2025-07-30 22:29:26 +01:00
Åke Strandberg
1ead01bc9a Explicitly pass config_entry to miele coordinator (#149691) 2025-07-30 20:19:01 +02:00
puddly
389a1251a1 Bump ZHA to 0.0.64 (#149683)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
Co-authored-by: abmantis <amfcalt@gmail.com>
2025-07-30 18:59:41 +01:00
Manu
8d27ca1e21 Fix KeyError in friends coordinator (#149684) 2025-07-30 19:59:01 +02:00
Michael Hansen
a76af50c10 Bump intents to 2025.7.30 (#149678) 2025-07-30 19:57:59 +02:00
Renat Sibgatulin
09b91bd76a Clean airq tests (#149682) 2025-07-30 18:48:36 +01:00
Jan Bouwhuis
736d582d04 Fix translation string reference for MQTT climate subentry option (#149673) 2025-07-30 18:53:21 +02:00
Bram Kragten
8114df4219 Bump version to 2025.9.0 (#149680) 2025-07-30 18:36:20 +02:00
293 changed files with 10431 additions and 2667 deletions

View File

@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 4
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.8"
HA_SHORT_VERSION: "2025.9"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version

View File

@@ -159,7 +159,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@2025.03.0
uses: home-assistant/wheels@2025.07.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -219,7 +219,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@2025.03.0
uses: home-assistant/wheels@2025.07.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2

View File

@@ -33,7 +33,10 @@ class AuthFlowContext(FlowContext, total=False):
redirect_uri: str
AuthFlowResult = FlowResult[AuthFlowContext, tuple[str, str]]
class AuthFlowResult(FlowResult[AuthFlowContext, tuple[str, str]], total=False):
"""Typed result dict for auth flow."""
result: Credentials # Only present if type is CREATE_ENTRY
@attr.s(slots=True)

View File

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

View File

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

View File

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

View File

@@ -268,7 +268,7 @@ class LoginFlowBaseView(HomeAssistantView):
result.pop("data")
result.pop("context")
result_obj: Credentials = result.pop("result")
result_obj = result.pop("result")
# Result can be None if credential was never linked to a user before.
user = await hass.auth.async_get_user_by_credentials(result_obj)
@@ -281,7 +281,8 @@ class LoginFlowBaseView(HomeAssistantView):
)
process_success_login(request)
result["result"] = self._store_result(client_id, result_obj)
# We overwrite the Credentials object with the string code to retrieve it.
result["result"] = self._store_result(client_id, result_obj) # type: ignore[typeddict-item]
return self.json(result)

View File

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

View File

@@ -2,9 +2,10 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from bsblan import BSBLAN, BSBLANConfig, BSBLANError
from bsblan import BSBLAN, BSBLANAuthError, BSBLANConfig, BSBLANError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -45,7 +46,7 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
self.username = user_input.get(CONF_USERNAME)
self.password = user_input.get(CONF_PASSWORD)
return await self._validate_and_create()
return await self._validate_and_create(user_input)
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
@@ -128,14 +129,29 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
self.username = user_input.get(CONF_USERNAME)
self.password = user_input.get(CONF_PASSWORD)
return await self._validate_and_create(is_discovery=True)
return await self._validate_and_create(user_input, is_discovery=True)
async def _validate_and_create(
self, is_discovery: bool = False
self, user_input: dict[str, Any], is_discovery: bool = False
) -> ConfigFlowResult:
"""Validate device connection and create entry."""
try:
await self._get_bsblan_info(is_discovery=is_discovery)
await self._get_bsblan_info()
except BSBLANAuthError:
if is_discovery:
return self.async_show_form(
step_id="discovery_confirm",
data_schema=vol.Schema(
{
vol.Optional(CONF_PASSKEY): str,
vol.Optional(CONF_USERNAME): str,
vol.Optional(CONF_PASSWORD): str,
}
),
errors={"base": "invalid_auth"},
description_placeholders={"host": str(self.host)},
)
return self._show_setup_form({"base": "invalid_auth"}, user_input)
except BSBLANError:
if is_discovery:
return self.async_show_form(
@@ -154,18 +170,145 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
return self._async_create_entry()
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth flow."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauth confirmation flow."""
existing_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
assert existing_entry
if user_input is None:
# Preserve existing values as defaults
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Optional(
CONF_PASSKEY,
default=existing_entry.data.get(
CONF_PASSKEY, vol.UNDEFINED
),
): str,
vol.Optional(
CONF_USERNAME,
default=existing_entry.data.get(
CONF_USERNAME, vol.UNDEFINED
),
): str,
vol.Optional(
CONF_PASSWORD,
default=vol.UNDEFINED,
): str,
}
),
)
# Use existing host and port, update auth credentials
self.host = existing_entry.data[CONF_HOST]
self.port = existing_entry.data[CONF_PORT]
self.passkey = user_input.get(CONF_PASSKEY) or existing_entry.data.get(
CONF_PASSKEY
)
self.username = user_input.get(CONF_USERNAME) or existing_entry.data.get(
CONF_USERNAME
)
self.password = user_input.get(CONF_PASSWORD)
try:
await self._get_bsblan_info(raise_on_progress=False, is_reauth=True)
except BSBLANAuthError:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Optional(
CONF_PASSKEY,
default=user_input.get(CONF_PASSKEY, vol.UNDEFINED),
): str,
vol.Optional(
CONF_USERNAME,
default=user_input.get(CONF_USERNAME, vol.UNDEFINED),
): str,
vol.Optional(
CONF_PASSWORD,
default=vol.UNDEFINED,
): str,
}
),
errors={"base": "invalid_auth"},
)
except BSBLANError:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Optional(
CONF_PASSKEY,
default=user_input.get(CONF_PASSKEY, vol.UNDEFINED),
): str,
vol.Optional(
CONF_USERNAME,
default=user_input.get(CONF_USERNAME, vol.UNDEFINED),
): str,
vol.Optional(
CONF_PASSWORD,
default=vol.UNDEFINED,
): str,
}
),
errors={"base": "cannot_connect"},
)
# Update the config entry with new auth data
data_updates = {}
if self.passkey is not None:
data_updates[CONF_PASSKEY] = self.passkey
if self.username is not None:
data_updates[CONF_USERNAME] = self.username
if self.password is not None:
data_updates[CONF_PASSWORD] = self.password
return self.async_update_reload_and_abort(
existing_entry, data_updates=data_updates, reason="reauth_successful"
)
@callback
def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult:
def _show_setup_form(
self, errors: dict | None = None, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Show the setup form to the user."""
# Preserve user input if provided, otherwise use defaults
defaults = user_input or {}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
vol.Optional(CONF_PASSKEY): str,
vol.Optional(CONF_USERNAME): str,
vol.Optional(CONF_PASSWORD): str,
vol.Required(
CONF_HOST, default=defaults.get(CONF_HOST, vol.UNDEFINED)
): str,
vol.Optional(
CONF_PORT, default=defaults.get(CONF_PORT, DEFAULT_PORT)
): int,
vol.Optional(
CONF_PASSKEY, default=defaults.get(CONF_PASSKEY, vol.UNDEFINED)
): str,
vol.Optional(
CONF_USERNAME,
default=defaults.get(CONF_USERNAME, vol.UNDEFINED),
): str,
vol.Optional(
CONF_PASSWORD,
default=defaults.get(CONF_PASSWORD, vol.UNDEFINED),
): str,
}
),
errors=errors or {},
@@ -186,7 +329,9 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
)
async def _get_bsblan_info(
self, raise_on_progress: bool = True, is_discovery: bool = False
self,
raise_on_progress: bool = True,
is_reauth: bool = False,
) -> None:
"""Get device information from a BSBLAN device."""
config = BSBLANConfig(
@@ -209,11 +354,13 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
format_mac(self.mac), raise_on_progress=raise_on_progress
)
# Always allow updating host/port for both user and discovery flows
# This ensures connectivity is maintained when devices change IP addresses
self._abort_if_unique_id_configured(
updates={
CONF_HOST: self.host,
CONF_PORT: self.port,
}
)
# Skip unique_id configuration check during reauth to prevent "already_configured" abort
if not is_reauth:
# Always allow updating host/port for both user and discovery flows
# This ensures connectivity is maintained when devices change IP addresses
self._abort_if_unique_id_configured(
updates={
CONF_HOST: self.host,
CONF_PORT: self.port,
}
)

View File

@@ -4,11 +4,19 @@ from dataclasses import dataclass
from datetime import timedelta
from random import randint
from bsblan import BSBLAN, BSBLANConnectionError, HotWaterState, Sensor, State
from bsblan import (
BSBLAN,
BSBLANAuthError,
BSBLANConnectionError,
HotWaterState,
Sensor,
State,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
@@ -62,6 +70,10 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
state = await self.client.state()
sensor = await self.client.sensor()
dhw = await self.client.hot_water_state()
except BSBLANAuthError as err:
raise ConfigEntryAuthFailed(
"Authentication failed for BSB-Lan device"
) from err
except BSBLANConnectionError as err:
host = self.config_entry.data[CONF_HOST] if self.config_entry else "unknown"
raise UpdateFailed(

View File

@@ -33,14 +33,25 @@
"username": "[%key:component::bsblan::config::step::user::data_description::username%]",
"password": "[%key:component::bsblan::config::step::user::data_description::password%]"
}
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The BSB-Lan integration needs to re-authenticate with {name}",
"data": {
"passkey": "[%key:component::bsblan::config::step::user::data::passkey%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"exceptions": {

View File

@@ -146,8 +146,9 @@ def _prepare_config_flow_result_json(
return prepare_result_json(result)
data = result.copy()
entry: config_entries.ConfigEntry = data["result"]
data["result"] = entry.as_json_fragment
entry: config_entries.ConfigEntry = data["result"] # type: ignore[typeddict-item]
# We overwrite the ConfigEntry object with its json representation.
data["result"] = entry.as_json_fragment # type: ignore[typeddict-unknown-key]
data.pop("data")
data.pop("context")
return data

View File

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

View File

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

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/emoncms",
"iot_class": "local_polling",
"requirements": ["pyemoncms==0.1.1"]
"requirements": ["pyemoncms==0.1.2"]
}

View File

@@ -12,12 +12,26 @@
},
"data_description": {
"url": "Server URL starting with the protocol (http or https)",
"api_key": "Your 32 bits API key"
"api_key": "Your 32 bits API key",
"sync_mode": "Pick your feeds manually (default) or synchronize them at once"
}
},
"choose_feeds": {
"data": {
"include_only_feed_id": "Choose feeds to include"
},
"data_description": {
"include_only_feed_id": "Pick the feeds you want to synchronize"
}
},
"reconfigure": {
"data": {
"url": "[%key:common::config_flow::data::url%]",
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"url": "[%key:component::emoncms::config::step::user::data_description::url%]",
"api_key": "[%key:component::emoncms::config::step::user::data_description::api_key%]"
}
}
},
@@ -30,8 +44,8 @@
"selector": {
"sync_mode": {
"options": {
"auto": "Synchronize all available Feeds",
"manual": "Select which Feeds to synchronize"
"auto": "Synchronize all available feeds",
"manual": "Select which feeds to synchronize"
}
}
},
@@ -89,6 +103,9 @@
"init": {
"data": {
"include_only_feed_id": "[%key:component::emoncms::config::step::choose_feeds::data::include_only_feed_id%]"
},
"data_description": {
"include_only_feed_id": "[%key:component::emoncms::config::step::choose_feeds::data_description::include_only_feed_id%]"
}
}
}

View File

@@ -116,6 +116,9 @@ 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_dict.pop("is_new", None)
device_entities.append({"device": device_dict, "entities": entities})
# remove envoy serial

View File

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

View File

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

View File

@@ -66,6 +66,26 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = {
key="last_alarm_type_name",
translation_key="last_alarm_type_name",
),
"Record_Mode": SensorEntityDescription(
key="Record_Mode",
translation_key="record_mode",
entity_registry_enabled_default=False,
),
"battery_camera_work_mode": SensorEntityDescription(
key="battery_camera_work_mode",
translation_key="battery_camera_work_mode",
entity_registry_enabled_default=False,
),
"powerStatus": SensorEntityDescription(
key="powerStatus",
translation_key="power_status",
entity_registry_enabled_default=False,
),
"OnlineStatus": SensorEntityDescription(
key="OnlineStatus",
translation_key="online_status",
entity_registry_enabled_default=False,
),
}
@@ -76,16 +96,26 @@ async def async_setup_entry(
) -> None:
"""Set up EZVIZ sensors based on a config entry."""
coordinator = entry.runtime_data
entities: list[EzvizSensor] = []
async_add_entities(
[
for camera, sensors in coordinator.data.items():
entities.extend(
EzvizSensor(coordinator, camera, sensor)
for camera in coordinator.data
for sensor, value in coordinator.data[camera].items()
if sensor in SENSOR_TYPES
if value is not None
]
)
for sensor, value in sensors.items()
if sensor in SENSOR_TYPES and value is not None
)
optionals = sensors.get("optionals", {})
entities.extend(
EzvizSensor(coordinator, camera, optional_key)
for optional_key in ("powerStatus", "OnlineStatus")
if optional_key in optionals
)
if "mode" in optionals.get("Record_Mode", {}):
entities.append(EzvizSensor(coordinator, camera, "mode"))
async_add_entities(entities)
class EzvizSensor(EzvizEntity, SensorEntity):

View File

@@ -147,6 +147,18 @@
},
"last_alarm_type_name": {
"name": "Last alarm type name"
},
"record_mode": {
"name": "Record mode"
},
"battery_camera_work_mode": {
"name": "Battery work mode"
},
"power_status": {
"name": "Power status"
},
"online_status": {
"name": "Online status"
}
},
"switch": {

View File

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

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/growatt_server",
"iot_class": "cloud_polling",
"loggers": ["growattServer"],
"requirements": ["growattServer==1.6.0"]
"requirements": ["growattServer==1.7.1"]
}

View File

@@ -9,6 +9,7 @@
"healthy": "Healthy",
"host_os": "Host operating system",
"installed_addons": "Installed add-ons",
"nameservers": "Nameservers",
"supervisor_api": "Supervisor API",
"supervisor_version": "Supervisor version",
"supported": "Supported",

View File

@@ -54,6 +54,15 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"error": "Unsupported",
}
nameservers = set()
for interface in network_info.get("interfaces", []):
if not interface.get("primary"):
continue
if ipv4 := interface.get("ipv4"):
nameservers.update(ipv4.get("nameservers", []))
if ipv6 := interface.get("ipv6"):
nameservers.update(ipv6.get("nameservers", []))
information = {
"host_os": host_info.get("operating_system"),
"update_channel": info.get("channel"),
@@ -62,6 +71,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"docker_version": info.get("docker"),
"disk_total": f"{host_info.get('disk_total')} GB",
"disk_used": f"{host_info.get('disk_used')} GB",
"nameservers": ", ".join(nameservers),
"healthy": healthy,
"supported": supported,
"host_connectivity": network_info.get("host_internet"),

View File

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

View File

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

View File

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

View File

@@ -628,12 +628,12 @@ class HomeAccessory(Accessory): # type: ignore[misc]
self,
domain: str,
service: str,
service_data: dict[str, Any] | None,
service_data: dict[str, Any],
value: Any | None = None,
) -> None:
"""Fire event and call service for changes from HomeKit."""
event_data = {
ATTR_ENTITY_ID: self.entity_id,
ATTR_ENTITY_ID: service_data.get(ATTR_ENTITY_ID, self.entity_id),
ATTR_DISPLAY_NAME: self.display_name,
ATTR_SERVICE: service,
ATTR_VALUE: value,

View File

@@ -57,6 +57,8 @@ CONF_LINKED_HUMIDITY_SENSOR = "linked_humidity_sensor"
CONF_LINKED_OBSTRUCTION_SENSOR = "linked_obstruction_sensor"
CONF_LINKED_PM25_SENSOR = "linked_pm25_sensor"
CONF_LINKED_TEMPERATURE_SENSOR = "linked_temperature_sensor"
CONF_LINKED_VALVE_DURATION = "linked_valve_duration"
CONF_LINKED_VALVE_END_TIME = "linked_valve_end_time"
CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold"
CONF_MAX_FPS = "max_fps"
CONF_MAX_HEIGHT = "max_height"
@@ -229,10 +231,12 @@ CHAR_ON = "On"
CHAR_OUTLET_IN_USE = "OutletInUse"
CHAR_POSITION_STATE = "PositionState"
CHAR_PROGRAMMABLE_SWITCH_EVENT = "ProgrammableSwitchEvent"
CHAR_REMAINING_DURATION = "RemainingDuration"
CHAR_REMOTE_KEY = "RemoteKey"
CHAR_ROTATION_DIRECTION = "RotationDirection"
CHAR_ROTATION_SPEED = "RotationSpeed"
CHAR_SATURATION = "Saturation"
CHAR_SET_DURATION = "SetDuration"
CHAR_SERIAL_NUMBER = "SerialNumber"
CHAR_SERVICE_LABEL_INDEX = "ServiceLabelIndex"
CHAR_SERVICE_LABEL_NAMESPACE = "ServiceLabelNamespace"

View File

@@ -15,6 +15,11 @@ from pyhap.const import (
)
from homeassistant.components import button, input_button
from homeassistant.components.input_number import (
ATTR_VALUE as INPUT_NUMBER_ATTR_VALUE,
DOMAIN as INPUT_NUMBER_DOMAIN,
SERVICE_SET_VALUE as INPUT_NUMBER_SERVICE_SET_VALUE,
)
from homeassistant.components.input_select import ATTR_OPTIONS, SERVICE_SELECT_OPTION
from homeassistant.components.lawn_mower import (
DOMAIN as LAWN_MOWER_DOMAIN,
@@ -45,6 +50,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, State, callback, split_entity_id
from homeassistant.helpers.event import async_call_later
from homeassistant.util import dt as dt_util
from .accessories import TYPES, HomeAccessory, HomeDriver
from .const import (
@@ -54,7 +60,11 @@ from .const import (
CHAR_NAME,
CHAR_ON,
CHAR_OUTLET_IN_USE,
CHAR_REMAINING_DURATION,
CHAR_SET_DURATION,
CHAR_VALVE_TYPE,
CONF_LINKED_VALVE_DURATION,
CONF_LINKED_VALVE_END_TIME,
SERV_OUTLET,
SERV_SWITCH,
SERV_VALVE,
@@ -271,7 +281,21 @@ class ValveBase(HomeAccessory):
self.on_service = on_service
self.off_service = off_service
serv_valve = self.add_preload_service(SERV_VALVE)
self.chars = []
self.linked_duration_entity: str | None = self.config.get(
CONF_LINKED_VALVE_DURATION
)
self.linked_end_time_entity: str | None = self.config.get(
CONF_LINKED_VALVE_END_TIME
)
if self.linked_duration_entity:
self.chars.append(CHAR_SET_DURATION)
if self.linked_end_time_entity:
self.chars.append(CHAR_REMAINING_DURATION)
serv_valve = self.add_preload_service(SERV_VALVE, self.chars)
self.char_active = serv_valve.configure_char(
CHAR_ACTIVE, value=False, setter_callback=self.set_state
)
@@ -279,6 +303,25 @@ class ValveBase(HomeAccessory):
self.char_valve_type = serv_valve.configure_char(
CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type].valve_type
)
if CHAR_SET_DURATION in self.chars:
_LOGGER.debug(
"%s: Add characteristic %s", self.entity_id, CHAR_SET_DURATION
)
self.char_set_duration = serv_valve.configure_char(
CHAR_SET_DURATION,
value=self.get_duration(),
setter_callback=self.set_duration,
)
if CHAR_REMAINING_DURATION in self.chars:
_LOGGER.debug(
"%s: Add characteristic %s", self.entity_id, CHAR_REMAINING_DURATION
)
self.char_remaining_duration = serv_valve.configure_char(
CHAR_REMAINING_DURATION, getter_callback=self.get_remaining_duration
)
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.async_update_state(state)
@@ -294,12 +337,75 @@ class ValveBase(HomeAccessory):
@callback
def async_update_state(self, new_state: State) -> None:
"""Update switch state after state changed."""
self._update_duration_chars()
current_state = 1 if new_state.state in self.open_states else 0
_LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state)
self.char_active.set_value(current_state)
_LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state)
self.char_in_use.set_value(current_state)
def _update_duration_chars(self) -> None:
"""Update valve duration related properties if characteristics are available."""
if CHAR_SET_DURATION in self.chars:
self.char_set_duration.set_value(self.get_duration())
if CHAR_REMAINING_DURATION in self.chars:
self.char_remaining_duration.set_value(self.get_remaining_duration())
def set_duration(self, value: int) -> None:
"""Set default duration for how long the valve should remain open."""
_LOGGER.debug("%s: Set default run time to %s", self.entity_id, value)
self.async_call_service(
INPUT_NUMBER_DOMAIN,
INPUT_NUMBER_SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: self.linked_duration_entity,
INPUT_NUMBER_ATTR_VALUE: value,
},
value,
)
def get_duration(self) -> int:
"""Get the default duration from Home Assistant."""
duration_state = self._get_entity_state(self.linked_duration_entity)
if duration_state is None:
_LOGGER.debug(
"%s: No linked duration entity state available", self.entity_id
)
return 0
try:
duration = float(duration_state)
return max(int(duration), 0)
except ValueError:
_LOGGER.debug("%s: Cannot parse linked duration entity", self.entity_id)
return 0
def get_remaining_duration(self) -> int:
"""Calculate the remaining duration based on end time in Home Assistant."""
end_time_state = self._get_entity_state(self.linked_end_time_entity)
if end_time_state is None:
_LOGGER.debug(
"%s: No linked end time entity state available", self.entity_id
)
return self.get_duration()
end_time = dt_util.parse_datetime(end_time_state)
if end_time is None:
_LOGGER.debug("%s: Cannot parse linked end time entity", self.entity_id)
return self.get_duration()
remaining_time = (end_time - dt_util.utcnow()).total_seconds()
return max(int(remaining_time), 0)
def _get_entity_state(self, entity_id: str | None) -> str | None:
"""Fetch the state of a linked entity."""
if entity_id is None:
return None
state = self.hass.states.get(entity_id)
if state is None:
return None
return state.state
@TYPES.register("ValveSwitch")
class ValveSwitch(ValveBase):

View File

@@ -17,6 +17,7 @@ import voluptuous as vol
from homeassistant.components import (
binary_sensor,
input_number,
media_player,
persistent_notification,
sensor,
@@ -69,6 +70,8 @@ from .const import (
CONF_LINKED_OBSTRUCTION_SENSOR,
CONF_LINKED_PM25_SENSOR,
CONF_LINKED_TEMPERATURE_SENSOR,
CONF_LINKED_VALVE_DURATION,
CONF_LINKED_VALVE_END_TIME,
CONF_LOW_BATTERY_THRESHOLD,
CONF_MAX_FPS,
CONF_MAX_HEIGHT,
@@ -266,7 +269,9 @@ SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend(
TYPE_VALVE,
)
),
)
),
vol.Optional(CONF_LINKED_VALVE_DURATION): cv.entity_domain(input_number.DOMAIN),
vol.Optional(CONF_LINKED_VALVE_END_TIME): cv.entity_domain(sensor.DOMAIN),
}
)
@@ -277,6 +282,12 @@ SENSOR_SCHEMA = BASIC_INFO_SCHEMA.extend(
}
)
VALVE_SCHEMA = BASIC_INFO_SCHEMA.extend(
{
vol.Optional(CONF_LINKED_VALVE_DURATION): cv.entity_domain(input_number.DOMAIN),
vol.Optional(CONF_LINKED_VALVE_END_TIME): cv.entity_domain(sensor.DOMAIN),
}
)
HOMEKIT_CHAR_TRANSLATIONS = {
0: " ", # nul
@@ -360,6 +371,9 @@ def validate_entity_config(values: dict) -> dict[str, dict]:
elif domain == "sensor":
config = SENSOR_SCHEMA(config)
elif domain == "valve":
config = VALVE_SCHEMA(config)
else:
config = BASIC_INFO_SCHEMA(config)

View File

@@ -283,19 +283,19 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity):
@property
def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
return self._device.doorState == DoorState.CLOSED
return self.functional_channel.doorState == DoorState.CLOSED
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
await self._device.send_door_command_async(DoorCommand.OPEN)
await self.functional_channel.async_send_door_command(DoorCommand.OPEN)
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
await self._device.send_door_command_async(DoorCommand.CLOSE)
await self.functional_channel.async_send_door_command(DoorCommand.CLOSE)
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
await self._device.send_door_command_async(DoorCommand.STOP)
await self.functional_channel.async_send_door_command(DoorCommand.STOP)
class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -289,17 +289,23 @@ class MotionTiltDevice(MotionPositionDevice):
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_angle, 180)
await self.async_request_position_till_stop()
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_angle, 0)
await self.async_request_position_till_stop()
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
angle = kwargs[ATTR_TILT_POSITION] * 180 / 100
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_angle, angle)
await self.async_request_position_till_stop()
async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
"""Stop the cover."""
async with self._api_lock:
@@ -360,11 +366,15 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Open)
await self.async_request_position_till_stop()
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Close)
await self.async_request_position_till_stop()
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
angle = kwargs[ATTR_TILT_POSITION]
@@ -376,6 +386,8 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_position, angle)
await self.async_request_position_till_stop()
async def async_set_absolute_position(self, **kwargs):
"""Move the cover to a specific absolute position (see TDBU)."""
angle = kwargs.get(ATTR_TILT_POSITION)
@@ -390,6 +402,8 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_position, angle)
await self.async_request_position_till_stop()
class MotionTDBUDevice(MotionBaseDevice):
"""Representation of a Motion Top Down Bottom Up blind Device."""

View File

@@ -42,6 +42,7 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind
self._requesting_position: CALLBACK_TYPE | None = None
self._previous_positions: list[int | dict | None] = []
self._previous_angles: list[int | None] = []
if blind.device_type in DEVICE_TYPES_WIFI:
self._update_interval_moving = UPDATE_INTERVAL_MOVING_WIFI
@@ -112,17 +113,27 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind
"""Request a state update from the blind at a scheduled point in time."""
# add the last position to the list and keep the list at max 2 items
self._previous_positions.append(self._blind.position)
self._previous_angles.append(self._blind.angle)
if len(self._previous_positions) > 2:
del self._previous_positions[: len(self._previous_positions) - 2]
if len(self._previous_angles) > 2:
del self._previous_angles[: len(self._previous_angles) - 2]
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Update_trigger)
self.coordinator.async_update_listeners()
if len(self._previous_positions) < 2 or not all(
self._blind.position == prev_position
for prev_position in self._previous_positions
if (
len(self._previous_positions) < 2
or not all(
self._blind.position == prev_position
for prev_position in self._previous_positions
)
or len(self._previous_angles) < 2
or not all(
self._blind.angle == prev_angle for prev_angle in self._previous_angles
)
):
# keep updating the position @self._update_interval_moving until the position does not change.
self._requesting_position = async_call_later(
@@ -132,6 +143,7 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind
)
else:
self._previous_positions = []
self._previous_angles = []
self._requesting_position = None
async def async_request_position_till_stop(self, delay: int | None = None) -> None:
@@ -140,7 +152,8 @@ class MotionCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinatorMotionBlind
delay = self._update_interval_moving
self._previous_positions = []
if self._blind.position is None:
self._previous_angles = []
if self._blind.position is None and self._blind.angle is None:
return
if self._requesting_position is not None:
self._requesting_position()

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from enum import StrEnum
from typing import TYPE_CHECKING
from psnawp_api.core.psnawp_exceptions import (
PSNAWPClientError,
@@ -10,12 +11,14 @@ from psnawp_api.core.psnawp_exceptions import (
PSNAWPNotFoundError,
PSNAWPServerError,
)
from psnawp_api.models.group.group import Group
from homeassistant.components.notify import (
DOMAIN as NOTIFY_DOMAIN,
NotifyEntity,
NotifyEntityDescription,
)
from homeassistant.config_entries import ConfigSubentry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
@@ -24,6 +27,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import (
PlaystationNetworkConfigEntry,
PlaystationNetworkFriendDataCoordinator,
PlaystationNetworkGroupsUpdateCoordinator,
)
from .entity import PlaystationNetworkServiceEntity
@@ -35,6 +39,7 @@ class PlaystationNetworkNotify(StrEnum):
"""PlayStation Network sensors."""
GROUP_MESSAGE = "group_message"
DIRECT_MESSAGE = "direct_message"
async def async_setup_entry(
@@ -45,6 +50,7 @@ async def async_setup_entry(
"""Set up the notify entity platform."""
coordinator = config_entry.runtime_data.groups
groups_added: set[str] = set()
entity_registry = er.async_get(hass)
@@ -72,8 +78,50 @@ async def async_setup_entry(
coordinator.async_add_listener(add_entities)
add_entities()
for subentry_id, friend_coordinator in config_entry.runtime_data.friends.items():
async_add_entities(
[
PlaystationNetworkDirectMessageNotifyEntity(
friend_coordinator,
config_entry.subentries[subentry_id],
)
],
config_subentry_id=subentry_id,
)
class PlaystationNetworkNotifyEntity(PlaystationNetworkServiceEntity, NotifyEntity):
class PlaystationNetworkNotifyBaseEntity(PlaystationNetworkServiceEntity, NotifyEntity):
"""Base class of PlayStation Network notify entity."""
group: Group | None = None
def send_message(self, message: str, title: str | None = None) -> None:
"""Send a message."""
if TYPE_CHECKING:
assert self.group
try:
self.group.send_message(message)
except PSNAWPNotFoundError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="group_invalid",
translation_placeholders=dict(self.translation_placeholders),
) from e
except PSNAWPForbiddenError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="send_message_forbidden",
translation_placeholders=dict(self.translation_placeholders),
) from e
except (PSNAWPServerError, PSNAWPClientError) as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="send_message_failed",
translation_placeholders=dict(self.translation_placeholders),
) from e
class PlaystationNetworkNotifyEntity(PlaystationNetworkNotifyBaseEntity):
"""Representation of a PlayStation Network notify entity."""
coordinator: PlaystationNetworkGroupsUpdateCoordinator
@@ -101,26 +149,31 @@ class PlaystationNetworkNotifyEntity(PlaystationNetworkServiceEntity, NotifyEnti
super().__init__(coordinator, self.entity_description)
class PlaystationNetworkDirectMessageNotifyEntity(PlaystationNetworkNotifyBaseEntity):
"""Representation of a PlayStation Network notify entity for sending direct messages."""
coordinator: PlaystationNetworkFriendDataCoordinator
def __init__(
self,
coordinator: PlaystationNetworkFriendDataCoordinator,
subentry: ConfigSubentry,
) -> None:
"""Initialize a notification entity."""
self.entity_description = NotifyEntityDescription(
key=PlaystationNetworkNotify.DIRECT_MESSAGE,
translation_key=PlaystationNetworkNotify.DIRECT_MESSAGE,
)
super().__init__(coordinator, self.entity_description, subentry)
def send_message(self, message: str, title: str | None = None) -> None:
"""Send a message."""
try:
self.group.send_message(message)
except PSNAWPNotFoundError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="group_invalid",
translation_placeholders=dict(self.translation_placeholders),
) from e
except PSNAWPForbiddenError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="send_message_forbidden",
translation_placeholders=dict(self.translation_placeholders),
) from e
except (PSNAWPServerError, PSNAWPClientError) as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="send_message_failed",
translation_placeholders=dict(self.translation_placeholders),
) from e
if not self.group:
self.group = self.coordinator.psn.psn.group(
users_list=[self.coordinator.user]
)
super().send_message(message, title)

View File

@@ -158,6 +158,9 @@
"notify": {
"group_message": {
"name": "Group: {group_name}"
},
"direct_message": {
"name": "Direct message"
}
}
}

View File

@@ -22,7 +22,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import QbusConfigEntry
from .entity import QbusEntity, add_new_outputs
from .entity import QbusEntity, create_new_entities
PARALLEL_UPDATES = 0
@@ -42,13 +42,13 @@ async def async_setup_entry(
added_outputs: list[QbusMqttOutput] = []
def _check_outputs() -> None:
add_new_outputs(
entities = create_new_entities(
coordinator,
added_outputs,
lambda output: output.type == "thermo",
QbusClimate,
async_add_entities,
)
async_add_entities(entities)
_check_outputs()
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))

View File

@@ -10,6 +10,7 @@ PLATFORMS: list[Platform] = [
Platform.COVER,
Platform.LIGHT,
Platform.SCENE,
Platform.SENSOR,
Platform.SWITCH,
]

View File

@@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import QbusConfigEntry
from .entity import QbusEntity, add_new_outputs
from .entity import QbusEntity, create_new_entities
PARALLEL_UPDATES = 0
@@ -36,13 +36,13 @@ async def async_setup_entry(
added_outputs: list[QbusMqttOutput] = []
def _check_outputs() -> None:
add_new_outputs(
entities = create_new_entities(
coordinator,
added_outputs,
lambda output: output.type == "shutter",
QbusCover,
async_add_entities,
)
async_add_entities(entities)
_check_outputs()
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))

View File

@@ -14,7 +14,6 @@ from qbusmqttapi.state import QbusMqttState
from homeassistant.components.mqtt import ReceiveMessage, client as mqtt
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, MANUFACTURER
from .coordinator import QbusControllerCoordinator
@@ -24,14 +23,24 @@ _REFID_REGEX = re.compile(r"^\d+\/(\d+(?:\/\d+)?)$")
StateT = TypeVar("StateT", bound=QbusMqttState)
def add_new_outputs(
def create_new_entities(
coordinator: QbusControllerCoordinator,
added_outputs: list[QbusMqttOutput],
filter_fn: Callable[[QbusMqttOutput], bool],
entity_type: type[QbusEntity],
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Call async_add_entities for new outputs."""
) -> list[QbusEntity]:
"""Create entities for new outputs."""
new_outputs = determine_new_outputs(coordinator, added_outputs, filter_fn)
return [entity_type(output) for output in new_outputs]
def determine_new_outputs(
coordinator: QbusControllerCoordinator,
added_outputs: list[QbusMqttOutput],
filter_fn: Callable[[QbusMqttOutput], bool],
) -> list[QbusMqttOutput]:
"""Determine new outputs."""
added_ref_ids = {k.ref_id for k in added_outputs}
@@ -43,7 +52,8 @@ def add_new_outputs(
if new_outputs:
added_outputs.extend(new_outputs)
async_add_entities([entity_type(output) for output in new_outputs])
return new_outputs
def format_ref_id(ref_id: str) -> str | None:
@@ -67,7 +77,13 @@ class QbusEntity(Entity, Generic[StateT], ABC):
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(self, mqtt_output: QbusMqttOutput) -> None:
def __init__(
self,
mqtt_output: QbusMqttOutput,
*,
id_suffix: str = "",
link_to_main_device: bool = False,
) -> None:
"""Initialize the Qbus entity."""
self._mqtt_output = mqtt_output
@@ -79,17 +95,25 @@ class QbusEntity(Entity, Generic[StateT], ABC):
)
ref_id = format_ref_id(mqtt_output.ref_id)
unique_id = f"ctd_{mqtt_output.device.serial_number}_{ref_id}"
self._attr_unique_id = f"ctd_{mqtt_output.device.serial_number}_{ref_id}"
if id_suffix:
unique_id += f"_{id_suffix}"
# Create linked device
self._attr_device_info = DeviceInfo(
name=mqtt_output.name.title(),
manufacturer=MANUFACTURER,
identifiers={(DOMAIN, f"{mqtt_output.device.serial_number}_{ref_id}")},
suggested_area=mqtt_output.location.title(),
via_device=create_main_device_identifier(mqtt_output),
)
self._attr_unique_id = unique_id
if link_to_main_device:
self._attr_device_info = DeviceInfo(
identifiers={create_main_device_identifier(mqtt_output)}
)
else:
self._attr_device_info = DeviceInfo(
name=mqtt_output.name.title(),
manufacturer=MANUFACTURER,
identifiers={(DOMAIN, f"{mqtt_output.device.serial_number}_{ref_id}")},
suggested_area=mqtt_output.location.title(),
via_device=create_main_device_identifier(mqtt_output),
)
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""

View File

@@ -11,7 +11,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.color import brightness_to_value, value_to_brightness
from .coordinator import QbusConfigEntry
from .entity import QbusEntity, add_new_outputs
from .entity import QbusEntity, create_new_entities
PARALLEL_UPDATES = 0
@@ -27,13 +27,13 @@ async def async_setup_entry(
added_outputs: list[QbusMqttOutput] = []
def _check_outputs() -> None:
add_new_outputs(
entities = create_new_entities(
coordinator,
added_outputs,
lambda output: output.type == "analog",
QbusLight,
async_add_entities,
)
async_add_entities(entities)
_check_outputs()
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))

View File

@@ -7,6 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/qbus",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["qbusmqttapi"],
"mqtt": [
"cloudapp/QBUSMQTTGW/state",
"cloudapp/QBUSMQTTGW/config",

View File

@@ -7,11 +7,10 @@ from qbusmqttapi.state import QbusMqttState, StateAction, StateType
from homeassistant.components.scene import Scene
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import QbusConfigEntry
from .entity import QbusEntity, add_new_outputs, create_main_device_identifier
from .entity import QbusEntity, create_new_entities
PARALLEL_UPDATES = 0
@@ -27,13 +26,13 @@ async def async_setup_entry(
added_outputs: list[QbusMqttOutput] = []
def _check_outputs() -> None:
add_new_outputs(
entities = create_new_entities(
coordinator,
added_outputs,
lambda output: output.type == "scene",
QbusScene,
async_add_entities,
)
async_add_entities(entities)
_check_outputs()
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))
@@ -45,12 +44,8 @@ class QbusScene(QbusEntity, Scene):
def __init__(self, mqtt_output: QbusMqttOutput) -> None:
"""Initialize scene entity."""
super().__init__(mqtt_output)
super().__init__(mqtt_output, link_to_main_device=True)
# Add to main controller device
self._attr_device_info = DeviceInfo(
identifiers={create_main_device_identifier(mqtt_output)}
)
self._attr_name = mqtt_output.name.title()
async def async_activate(self, **kwargs: Any) -> None:

View File

@@ -0,0 +1,378 @@
"""Support for Qbus sensor."""
from dataclasses import dataclass
from qbusmqttapi.discovery import QbusMqttOutput
from qbusmqttapi.state import (
GaugeStateProperty,
QbusMqttGaugeState,
QbusMqttHumidityState,
QbusMqttThermoState,
QbusMqttVentilationState,
QbusMqttWeatherState,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfLength,
UnitOfPower,
UnitOfPressure,
UnitOfSoundPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolume,
UnitOfVolumeFlowRate,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import QbusConfigEntry
from .entity import QbusEntity, create_new_entities, determine_new_outputs
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class QbusWeatherDescription(SensorEntityDescription):
"""Description for Qbus weather entities."""
property: str
_WEATHER_DESCRIPTIONS = (
QbusWeatherDescription(
key="daylight",
property="dayLight",
translation_key="daylight",
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=LIGHT_LUX,
),
QbusWeatherDescription(
key="light",
property="light",
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=LIGHT_LUX,
),
QbusWeatherDescription(
key="light_east",
property="lightEast",
translation_key="light_east",
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=LIGHT_LUX,
),
QbusWeatherDescription(
key="light_south",
property="lightSouth",
translation_key="light_south",
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=LIGHT_LUX,
),
QbusWeatherDescription(
key="light_west",
property="lightWest",
translation_key="light_west",
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=LIGHT_LUX,
),
QbusWeatherDescription(
key="temperature",
property="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
QbusWeatherDescription(
key="wind",
property="wind",
device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
),
)
_GAUGE_VARIANT_DESCRIPTIONS = {
"AIRPRESSURE": SensorEntityDescription(
key="airpressure",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.MBAR,
state_class=SensorStateClass.MEASUREMENT,
),
"AIRQUALITY": SensorEntityDescription(
key="airquality",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
"CURRENT": SensorEntityDescription(
key="current",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
),
"ENERGY": SensorEntityDescription(
key="energy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL,
),
"GAS": SensorEntityDescription(
key="gas",
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
),
"GASFLOW": SensorEntityDescription(
key="gasflow",
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
),
"HUMIDITY": SensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
"LIGHT": SensorEntityDescription(
key="light",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
),
"LOUDNESS": SensorEntityDescription(
key="loudness",
device_class=SensorDeviceClass.SOUND_PRESSURE,
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
state_class=SensorStateClass.MEASUREMENT,
),
"POWER": SensorEntityDescription(
key="power",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
),
"PRESSURE": SensorEntityDescription(
key="pressure",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.KPA,
state_class=SensorStateClass.MEASUREMENT,
),
"TEMPERATURE": SensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
"VOLTAGE": SensorEntityDescription(
key="voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
),
"VOLUME": SensorEntityDescription(
key="volume",
device_class=SensorDeviceClass.VOLUME_STORAGE,
native_unit_of_measurement=UnitOfVolume.LITERS,
state_class=SensorStateClass.MEASUREMENT,
),
"WATER": SensorEntityDescription(
key="water",
device_class=SensorDeviceClass.WATER,
native_unit_of_measurement=UnitOfVolume.LITERS,
state_class=SensorStateClass.TOTAL,
),
"WATERFLOW": SensorEntityDescription(
key="waterflow",
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
),
"WATERLEVEL": SensorEntityDescription(
key="waterlevel",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.METERS,
state_class=SensorStateClass.MEASUREMENT,
),
"WATERPRESSURE": SensorEntityDescription(
key="waterpressure",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.MBAR,
state_class=SensorStateClass.MEASUREMENT,
),
"WIND": SensorEntityDescription(
key="wind",
device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT,
),
}
def _is_gauge_with_variant(output: QbusMqttOutput) -> bool:
return (
output.type == "gauge"
and isinstance(output.variant, str)
and _GAUGE_VARIANT_DESCRIPTIONS.get(output.variant.upper()) is not None
)
def _is_ventilation_with_co2(output: QbusMqttOutput) -> bool:
return output.type == "ventilation" and output.properties.get("co2") is not None
async def async_setup_entry(
hass: HomeAssistant,
entry: QbusConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up sensor entities."""
coordinator = entry.runtime_data
added_outputs: list[QbusMqttOutput] = []
def _create_weather_entities() -> list[QbusEntity]:
new_outputs = determine_new_outputs(
coordinator, added_outputs, lambda output: output.type == "weatherstation"
)
return [
QbusWeatherSensor(output, description)
for output in new_outputs
for description in _WEATHER_DESCRIPTIONS
]
def _check_outputs() -> None:
entities: list[QbusEntity] = [
*create_new_entities(
coordinator,
added_outputs,
_is_gauge_with_variant,
QbusGaugeVariantSensor,
),
*create_new_entities(
coordinator,
added_outputs,
lambda output: output.type == "humidity",
QbusHumiditySensor,
),
*create_new_entities(
coordinator,
added_outputs,
lambda output: output.type == "thermo",
QbusThermoSensor,
),
*create_new_entities(
coordinator,
added_outputs,
_is_ventilation_with_co2,
QbusVentilationSensor,
),
*_create_weather_entities(),
]
async_add_entities(entities)
_check_outputs()
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))
class QbusGaugeVariantSensor(QbusEntity, SensorEntity):
"""Representation of a Qbus sensor entity for gauges with variant."""
_state_cls = QbusMqttGaugeState
_attr_name = None
_attr_suggested_display_precision = 2
def __init__(self, mqtt_output: QbusMqttOutput) -> None:
"""Initialize sensor entity."""
super().__init__(mqtt_output)
variant = str(mqtt_output.variant)
self.entity_description = _GAUGE_VARIANT_DESCRIPTIONS[variant.upper()]
async def _handle_state_received(self, state: QbusMqttGaugeState) -> None:
self._attr_native_value = state.read_value(GaugeStateProperty.CURRENT_VALUE)
class QbusHumiditySensor(QbusEntity, SensorEntity):
"""Representation of a Qbus sensor entity for humidity modules."""
_state_cls = QbusMqttHumidityState
_attr_device_class = SensorDeviceClass.HUMIDITY
_attr_name = None
_attr_native_unit_of_measurement = PERCENTAGE
_attr_state_class = SensorStateClass.MEASUREMENT
async def _handle_state_received(self, state: QbusMqttHumidityState) -> None:
self._attr_native_value = state.read_value()
class QbusThermoSensor(QbusEntity, SensorEntity):
"""Representation of a Qbus sensor entity for thermostats."""
_state_cls = QbusMqttThermoState
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
_attr_state_class = SensorStateClass.MEASUREMENT
async def _handle_state_received(self, state: QbusMqttThermoState) -> None:
self._attr_native_value = state.read_current_temperature()
class QbusVentilationSensor(QbusEntity, SensorEntity):
"""Representation of a Qbus sensor entity for ventilations."""
_state_cls = QbusMqttVentilationState
_attr_device_class = SensorDeviceClass.CO2
_attr_name = None
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_suggested_display_precision = 0
async def _handle_state_received(self, state: QbusMqttVentilationState) -> None:
self._attr_native_value = state.read_co2()
class QbusWeatherSensor(QbusEntity, SensorEntity):
"""Representation of a Qbus weather sensor."""
_state_cls = QbusMqttWeatherState
entity_description: QbusWeatherDescription
def __init__(
self, mqtt_output: QbusMqttOutput, description: QbusWeatherDescription
) -> None:
"""Initialize sensor entity."""
super().__init__(mqtt_output, id_suffix=description.key)
self.entity_description = description
if description.key == "temperature":
self._attr_name = None
async def _handle_state_received(self, state: QbusMqttWeatherState) -> None:
if value := state.read_property(self.entity_description.property, None):
self.native_value = value

View File

@@ -16,6 +16,22 @@
"no_controller": "No controllers were found"
}
},
"entity": {
"sensor": {
"daylight": {
"name": "Daylight"
},
"light_east": {
"name": "Illuminance east"
},
"light_south": {
"name": "Illuminance south"
},
"light_west": {
"name": "Illuminance west"
}
}
},
"exceptions": {
"invalid_preset": {
"message": "Preset mode \"{preset}\" is not valid. Valid preset modes are: {options}."

View File

@@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import QbusConfigEntry
from .entity import QbusEntity, add_new_outputs
from .entity import QbusEntity, create_new_entities
PARALLEL_UPDATES = 0
@@ -26,13 +26,13 @@ async def async_setup_entry(
added_outputs: list[QbusMqttOutput] = []
def _check_outputs() -> None:
add_new_outputs(
entities = create_new_entities(
coordinator,
added_outputs,
lambda output: output.type == "onoff",
QbusSwitch,
async_add_entities,
)
async_add_entities(entities)
_check_outputs()
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))

View File

@@ -82,6 +82,7 @@
},
"sensor_device_class": {
"options": {
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
"area": "[%key:component::sensor::entity_component::area::name%]",
@@ -129,7 +130,7 @@
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]",
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
"volume": "[%key:component::sensor::entity_component::volume::name%]",
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",

View File

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

View File

@@ -89,8 +89,6 @@ class RepairsFlowManager(data_entry_flow.FlowManager):
"""
if result.get("type") != data_entry_flow.FlowResultType.ABORT:
ir.async_delete_issue(self.hass, flow.handler, flow.init_data["issue_id"])
if "result" not in result:
result["result"] = None
return result

View File

@@ -139,6 +139,7 @@
"selector": {
"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%]",
@@ -155,6 +156,7 @@
"distance": "[%key:component::sensor::entity_component::distance::name%]",
"duration": "[%key:component::sensor::entity_component::duration::name%]",
"energy": "[%key:component::sensor::entity_component::energy::name%]",
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
"gas": "[%key:component::sensor::entity_component::gas::name%]",
@@ -184,13 +186,14 @@
"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%]",
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
"water": "[%key:component::sensor::entity_component::water::name%]",
"weight": "[%key:component::sensor::entity_component::weight::name%]",
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
}
},

View File

@@ -71,10 +71,13 @@
"selector": {
"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%]",
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
"battery": "[%key:component::sensor::entity_component::battery::name%]",
"blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]",
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
@@ -85,6 +88,7 @@
"distance": "[%key:component::sensor::entity_component::distance::name%]",
"duration": "[%key:component::sensor::entity_component::duration::name%]",
"energy": "[%key:component::sensor::entity_component::energy::name%]",
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
"gas": "[%key:component::sensor::entity_component::gas::name%]",
@@ -115,13 +119,14 @@
"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%]",
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
"water": "[%key:component::sensor::entity_component::water::name%]",
"weight": "[%key:component::sensor::entity_component::weight::name%]",
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
}
},

View File

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

View File

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

View File

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

View File

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

View File

@@ -247,11 +247,15 @@ class TeslaFleetEnergySiteHistoryCoordinator(DataUpdateCoordinator[dict[str, Any
raise UpdateFailed(e.message) from e
self.updated_once = True
if not data or not isinstance(data.get("time_series"), list):
raise UpdateFailed("Received invalid data")
# Add all time periods together
output = dict.fromkeys(ENERGY_HISTORY_FIELDS, 0)
for period in data.get("time_series", []):
for key in ENERGY_HISTORY_FIELDS:
output[key] += period.get(key, 0)
if key in period:
output[key] += period[key]
return output

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble",
"iot_class": "local_push",
"requirements": ["xiaomi-ble==1.1.0"]
"requirements": ["xiaomi-ble==1.2.0"]
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -105,7 +105,6 @@ from .const import (
CONF_USB_PATH,
CONF_USE_ADDON,
DOMAIN,
DRIVER_READY_TIMEOUT,
EVENT_DEVICE_ADDED_TO_REGISTRY,
EVENT_VALUE_UPDATED,
LIB_LOGGER,
@@ -136,6 +135,7 @@ from .models import ZwaveJSConfigEntry, ZwaveJSData
from .services import async_setup_services
CONNECT_TIMEOUT = 10
DRIVER_READY_TIMEOUT = 60
CONFIG_SCHEMA = vol.Schema(
{
@@ -368,6 +368,16 @@ class DriverEvents:
)
)
# listen for driver ready event to reload the config entry
self.config_entry.async_on_unload(
driver.on(
"driver ready",
lambda _: self.hass.config_entries.async_schedule_reload(
self.config_entry.entry_id
),
)
)
# listen for new nodes being added to the mesh
self.config_entry.async_on_unload(
controller.on(
@@ -1074,23 +1084,32 @@ async def client_listen(
try:
await client.listen(driver_ready)
except BaseZwaveJSServerError as err:
if entry.state is not ConfigEntryState.LOADED:
if entry.state is ConfigEntryState.SETUP_IN_PROGRESS:
raise
LOGGER.error("Client listen failed: %s", err)
except Exception as err:
# We need to guard against unknown exceptions to not crash this task.
LOGGER.exception("Unexpected exception: %s", err)
if entry.state is not ConfigEntryState.LOADED:
if entry.state is ConfigEntryState.SETUP_IN_PROGRESS:
raise
if hass.is_stopping or entry.state is ConfigEntryState.UNLOAD_IN_PROGRESS:
return
if entry.state is ConfigEntryState.SETUP_IN_PROGRESS:
raise HomeAssistantError("Listen task ended unexpectedly")
# The entry needs to be reloaded since a new driver state
# will be acquired on reconnect.
# All model instances will be replaced when the new state is acquired.
if not hass.is_stopping:
if entry.state is not ConfigEntryState.LOADED:
raise HomeAssistantError("Listen task ended unexpectedly")
if entry.state.recoverable:
LOGGER.debug("Disconnected from server. Reloading integration")
hass.config_entries.async_schedule_reload(entry.entry_id)
else:
LOGGER.error(
"Disconnected from server. Cannot recover entry %s",
entry.title,
)
async def async_unload_entry(hass: HomeAssistant, entry: ZwaveJSConfigEntry) -> bool:

View File

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

View File

@@ -62,9 +62,12 @@ from .const import (
CONF_USB_PATH,
CONF_USE_ADDON,
DOMAIN,
DRIVER_READY_TIMEOUT,
)
from .helpers import CannotConnect, async_get_version_info
from .helpers import (
CannotConnect,
async_get_version_info,
async_wait_for_driver_ready_event,
)
from .models import ZwaveJSConfigEntry
_LOGGER = logging.getLogger(__name__)
@@ -1396,19 +1399,15 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
event["bytesWritten"] / event["total"] * 0.5 + 0.5
)
@callback
def set_driver_ready(event: dict) -> None:
"Set the driver ready event."
wait_driver_ready.set()
driver = self._get_driver()
controller = driver.controller
wait_driver_ready = asyncio.Event()
unsubs = [
controller.on("nvm convert progress", forward_progress),
controller.on("nvm restore progress", forward_progress),
driver.once("driver ready", set_driver_ready),
]
wait_for_driver_ready = async_wait_for_driver_ready_event(config_entry, driver)
try:
await controller.async_restore_nvm(
self.backup_data, {"preserveRoutes": False}
@@ -1417,8 +1416,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
raise AbortFlow(f"Failed to restore network: {err}") from err
else:
with suppress(TimeoutError):
async with asyncio.timeout(DRIVER_READY_TIMEOUT):
await wait_driver_ready.wait()
await wait_for_driver_ready()
try:
version_info = await async_get_version_info(
self.hass, config_entry.data[CONF_URL]
@@ -1435,10 +1433,10 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
self.hass.config_entries.async_update_entry(
config_entry, unique_id=str(version_info.home_id)
)
await self.hass.config_entries.async_reload(config_entry.entry_id)
# Reload the config entry two times to clean up
# the stale device entry.
# The config entry will be also be reloaded when the driver is ready,
# by the listener in the package module,
# and two reloads are needed to clean up the stale controller device entry.
# Since both the old and the new controller have the same node id,
# but different hardware identifiers, the integration
# will create a new device for the new controller, on the first reload,

View File

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

View File

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

View File

@@ -298,8 +298,10 @@ class ConfigFlowContext(FlowContext, total=False):
class ConfigFlowResult(FlowResult[ConfigFlowContext, str], total=False):
"""Typed result dict for config flow."""
# Extra keys, only present if type is CREATE_ENTRY
minor_version: int
options: Mapping[str, Any]
result: ConfigEntry
subentries: Iterable[ConfigSubentryData]
version: int
@@ -3345,7 +3347,6 @@ class ConfigSubentryFlowManager(
),
)
result["result"] = True
return result
@@ -3508,7 +3509,6 @@ class OptionsFlowManager(
):
self.hass.config_entries.async_schedule_reload(entry.entry_id)
result["result"] = True
return result
async def _async_setup_preview(

View File

@@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 8
MINOR_VERSION: Final = 9
PATCH_VERSION: Final = "0.dev0"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"

View File

@@ -142,7 +142,6 @@ class FlowResult(TypedDict, Generic[_FlowContextT, _HandlerT], total=False):
progress_task: asyncio.Task[Any] | None
reason: str
required: bool
result: Any
step_id: str
title: str
translation_domain: str
@@ -677,9 +676,10 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]):
and key in suggested_values
):
new_section_key = copy.copy(key)
schema[new_section_key] = val
val.schema = self.add_suggested_values_to_schema(
val.schema, suggested_values[key]
new_val = copy.copy(val)
schema[new_section_key] = new_val
new_val.schema = self.add_suggested_values_to_schema(
new_val.schema, suggested_values[key]
)
continue
@@ -706,10 +706,7 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]):
last_step: bool | None = None,
preview: str | None = None,
) -> _FlowResultT:
"""Return the definition of a form to gather user input.
The step_id parameter is deprecated and will be removed in a future release.
"""
"""Return the definition of a form to gather user input."""
flow_result = self._flow_result(
type=FlowResultType.FORM,
flow_id=self.flow_id,
@@ -771,10 +768,7 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]):
url: str,
description_placeholders: Mapping[str, str] | None = None,
) -> _FlowResultT:
"""Return the definition of an external step for the user to take.
The step_id parameter is deprecated and will be removed in a future release.
"""
"""Return the definition of an external step for the user to take."""
flow_result = self._flow_result(
type=FlowResultType.EXTERNAL_STEP,
flow_id=self.flow_id,
@@ -805,10 +799,7 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]):
description_placeholders: Mapping[str, str] | None = None,
progress_task: asyncio.Task[Any] | None = None,
) -> _FlowResultT:
"""Show a progress message to the user, without user input allowed.
The step_id parameter is deprecated and will be removed in a future release.
"""
"""Show a progress message to the user, without user input allowed."""
if progress_task is None and not self.__no_progress_task_reported:
self.__no_progress_task_reported = True
cls = self.__class__
@@ -868,7 +859,6 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]):
"""Show a navigation menu to the user.
Options dict maps step_id => i18n label
The step_id parameter is deprecated and will be removed in a future release.
"""
flow_result = self._flow_result(
type=FlowResultType.MENU,

View File

@@ -35,7 +35,7 @@ class _BaseFlowManagerView(HomeAssistantView, Generic[_FlowManagerT]):
"""Convert result to JSON."""
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
data = result.copy()
data.pop("result")
assert "result" not in result
data.pop("data")
data.pop("context")
return data

View File

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

View File

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

View File

@@ -608,10 +608,15 @@ async def async_get_all_descriptions(
new_descriptions_cache = descriptions_cache.copy()
for missing_trigger in missing_triggers:
domain = triggers[missing_trigger]
trigger_description_key = (
platform_and_sub_type[1]
if len(platform_and_sub_type := missing_trigger.split(".")) > 1
else missing_trigger
)
if (
yaml_description := new_triggers_descriptions.get(domain, {}).get( # type: ignore[union-attr]
missing_trigger
trigger_description_key
)
) is None:
_LOGGER.debug(

View File

@@ -38,8 +38,8 @@ habluetooth==4.0.1
hass-nabucasa==0.110.0
hassil==2.2.3
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250730.0
home-assistant-intents==2025.6.23
home-assistant-frontend==20250731.0
home-assistant-intents==2025.7.30
httpx==0.28.1
ifaddr==0.2.0
Jinja2==3.1.6
@@ -209,7 +209,6 @@ aiofiles>=24.1.0
# https://github.com/aio-libs/multidict/issues/1131
multidict>=6.4.2
# rpds-py > 0.25.0 requires cargo 1.84.0
# Stable Alpine current only ships cargo 1.83.0
# rpds-py frequently updates cargo causing build failures
# No wheels upstream available for armhf & armv7
rpds-py==0.24.0
rpds-py==0.26.0

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.8.0.dev0"
version = "2025.9.0.dev0"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
@@ -487,19 +487,10 @@ filterwarnings = [
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteofrance_api.model.forecast",
# -- fixed, waiting for release / update
# https://github.com/DataDog/datadogpy/pull/290 - >=0.23.0
"ignore:.*invalid escape sequence:SyntaxWarning:.*datadog.dogstatsd.base",
# https://github.com/DataDog/datadogpy/pull/566/files - >=0.37.0
"ignore:pkg_resources is deprecated as an API:UserWarning:datadog.util.compat",
# https://github.com/httplib2/httplib2/pull/226 - >=0.21.0
"ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:httplib2",
# https://github.com/vacanza/python-holidays/discussions/1800 - >1.0.0
"ignore::DeprecationWarning:holidays",
# https://github.com/ReactiveX/RxPY/pull/716 - >4.0.4
"ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:reactivex.internal.constants",
# https://github.com/postlund/pyatv/issues/2645 - >0.16.0
# https://github.com/postlund/pyatv/pull/2664
"ignore:Protobuf gencode .* exactly one major version older than the runtime version 6.* at pyatv:UserWarning:google.protobuf.runtime_version",
# https://github.com/rytilahti/python-miio/pull/1809 - >=0.6.0.dev0
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.protocol",
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.miioprotocol",
@@ -526,6 +517,9 @@ filterwarnings = [
"ignore:loop argument is deprecated:DeprecationWarning:emulated_roku",
# https://pypi.org/project/foobot_async/ - v1.0.1 - 2024-08-16
"ignore:with timeout\\(\\) is deprecated:DeprecationWarning:foobot_async",
# https://pypi.org/project/motionblindsble/ - v0.1.3 - 2024-11-12
# https://github.com/LennP/motionblindsble/blob/0.1.3/motionblindsble/device.py#L390
"ignore:Passing additional arguments for BLEDevice is deprecated and has no effect:DeprecationWarning:motionblindsble.device",
# https://pypi.org/project/pyeconet/ - v0.1.28 - 2025-02-15
# https://github.com/w1ll1am23/pyeconet/blob/v0.1.28/src/pyeconet/api.py#L38
"ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:pyeconet.api",
@@ -542,8 +536,6 @@ filterwarnings = [
"ignore:Callback API version 1 is deprecated, update to latest version:DeprecationWarning:roborock.cloud_api",
# https://github.com/briis/pyweatherflowudp/blob/v1.4.5/pyweatherflowudp/const.py#L20 - v1.4.5 - 2023-10-10
"ignore:This function will be removed in future versions of pint:DeprecationWarning:pyweatherflowudp.const",
# New in aiohttp - v3.9.0
"ignore:It is recommended to use web.AppKey instances for keys:UserWarning:(homeassistant|tests|aiohttp_cors)",
# - SyntaxWarnings
# https://pypi.org/project/aprslib/ - v0.7.2 - 2022-07-10
"ignore:.*invalid escape sequence:SyntaxWarning:.*aprslib.parsing.common",
@@ -589,7 +581,7 @@ filterwarnings = [
# -- Websockets 14.1
# https://websockets.readthedocs.io/en/stable/howto/upgrade.html
"ignore:websockets.legacy is deprecated:DeprecationWarning:websockets.legacy",
# https://github.com/graphql-python/gql/pull/543 - >=4.0.0a0
# https://github.com/graphql-python/gql/pull/543 - >=4.0.0b0
"ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning:gql.transport.websockets_base",
# -- unmaintained projects, last release about 2+ years

26
requirements_all.txt generated
View File

@@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==37.1.5
aioesphomeapi==37.2.2
# homeassistant.components.flo
aioflo==2021.11.0
@@ -310,7 +310,7 @@ aiolookin==1.0.0
aiolyric==2.0.1
# homeassistant.components.mealie
aiomealie==0.10.0
aiomealie==0.10.1
# homeassistant.components.modern_forms
aiomodernforms==0.1.8
@@ -791,7 +791,7 @@ deluge-client==1.10.2
demetriek==1.3.0
# homeassistant.components.denonavr
denonavr==1.1.1
denonavr==1.1.2
# homeassistant.components.devialet
devialet==1.5.7
@@ -1100,7 +1100,7 @@ greenwavereality==0.5.1
gridnet==5.0.1
# homeassistant.components.growatt_server
growattServer==1.6.0
growattServer==1.7.1
# homeassistant.components.google_sheets
gspread==5.5.0
@@ -1174,10 +1174,10 @@ hole==0.9.0
holidays==0.77
# homeassistant.components.frontend
home-assistant-frontend==20250730.0
home-assistant-frontend==20250731.0
# homeassistant.components.conversation
home-assistant-intents==2025.6.23
home-assistant-intents==2025.7.30
# homeassistant.components.homematicip_cloud
homematicip==2.2.0
@@ -1458,7 +1458,7 @@ monzopy==1.5.1
mopeka-iot-ble==0.8.0
# homeassistant.components.motion_blinds
motionblinds==0.6.29
motionblinds==0.6.30
# homeassistant.components.motionblinds_ble
motionblindsble==0.1.3
@@ -1963,7 +1963,7 @@ pyefergy==22.5.0
pyegps==0.2.5
# homeassistant.components.emoncms
pyemoncms==0.1.1
pyemoncms==0.1.2
# homeassistant.components.enphase_envoy
pyenphase==2.2.3
@@ -2122,7 +2122,7 @@ pylibrespot-java==0.1.1
pylitejet==0.6.3
# homeassistant.components.litterrobot
pylitterbot==2024.2.2
pylitterbot==2024.2.3
# homeassistant.components.lutron_caseta
pylutron-caseta==0.24.0
@@ -2666,7 +2666,7 @@ renault-api==0.3.1
renson-endura-delta==1.7.2
# homeassistant.components.reolink
reolink-aio==0.14.4
reolink-aio==0.14.5
# homeassistant.components.idteck_prox
rfk101py==0.0.1
@@ -3057,7 +3057,7 @@ venstarcolortouch==0.21
vilfo-api-client==0.5.0
# homeassistant.components.voip
voip-utils==0.3.3
voip-utils==0.3.4
# homeassistant.components.volkszaehler
volkszaehler==0.4.0
@@ -3139,7 +3139,7 @@ wyoming==1.7.1
xbox-webapi==2.1.0
# homeassistant.components.xiaomi_ble
xiaomi-ble==1.1.0
xiaomi-ble==1.2.0
# homeassistant.components.knx
xknx==3.8.0
@@ -3203,7 +3203,7 @@ zeroconf==0.147.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.62
zha==0.0.64
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

@@ -235,7 +235,7 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==37.1.5
aioesphomeapi==37.2.2
# homeassistant.components.flo
aioflo==2021.11.0
@@ -292,7 +292,7 @@ aiolookin==1.0.0
aiolyric==2.0.1
# homeassistant.components.mealie
aiomealie==0.10.0
aiomealie==0.10.1
# homeassistant.components.modern_forms
aiomodernforms==0.1.8
@@ -691,7 +691,7 @@ deluge-client==1.10.2
demetriek==1.3.0
# homeassistant.components.denonavr
denonavr==1.1.1
denonavr==1.1.2
# homeassistant.components.devialet
devialet==1.5.7
@@ -961,7 +961,7 @@ greeneye_monitor==3.0.3
gridnet==5.0.1
# homeassistant.components.growatt_server
growattServer==1.6.0
growattServer==1.7.1
# homeassistant.components.google_sheets
gspread==5.5.0
@@ -1023,10 +1023,10 @@ hole==0.9.0
holidays==0.77
# homeassistant.components.frontend
home-assistant-frontend==20250730.0
home-assistant-frontend==20250731.0
# homeassistant.components.conversation
home-assistant-intents==2025.6.23
home-assistant-intents==2025.7.30
# homeassistant.components.homematicip_cloud
homematicip==2.2.0
@@ -1250,7 +1250,7 @@ monzopy==1.5.1
mopeka-iot-ble==0.8.0
# homeassistant.components.motion_blinds
motionblinds==0.6.29
motionblinds==0.6.30
# homeassistant.components.motionblinds_ble
motionblindsble==0.1.3
@@ -1638,7 +1638,7 @@ pyefergy==22.5.0
pyegps==0.2.5
# homeassistant.components.emoncms
pyemoncms==0.1.1
pyemoncms==0.1.2
# homeassistant.components.enphase_envoy
pyenphase==2.2.3
@@ -1767,7 +1767,7 @@ pylibrespot-java==0.1.1
pylitejet==0.6.3
# homeassistant.components.litterrobot
pylitterbot==2024.2.2
pylitterbot==2024.2.3
# homeassistant.components.lutron_caseta
pylutron-caseta==0.24.0
@@ -2212,7 +2212,7 @@ renault-api==0.3.1
renson-endura-delta==1.7.2
# homeassistant.components.reolink
reolink-aio==0.14.4
reolink-aio==0.14.5
# homeassistant.components.rflink
rflink==0.0.67
@@ -2525,7 +2525,7 @@ venstarcolortouch==0.21
vilfo-api-client==0.5.0
# homeassistant.components.voip
voip-utils==0.3.3
voip-utils==0.3.4
# homeassistant.components.volvo
volvocarsapi==0.4.1
@@ -2592,7 +2592,7 @@ wyoming==1.7.1
xbox-webapi==2.1.0
# homeassistant.components.xiaomi_ble
xiaomi-ble==1.1.0
xiaomi-ble==1.2.0
# homeassistant.components.knx
xknx==3.8.0
@@ -2647,7 +2647,7 @@ zeroconf==0.147.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.62
zha==0.0.64
# homeassistant.components.zwave_js
zwave-js-server-python==0.67.0

View File

@@ -4,7 +4,7 @@
# Stop on errors
set -e
cd "$(dirname "$0")/.."
cd "$(realpath "$(dirname "$0")/..")"
echo "Installing development dependencies..."
uv pip install wheel --constraint homeassistant/package_constraints.txt --upgrade

View File

@@ -235,10 +235,9 @@ aiofiles>=24.1.0
# https://github.com/aio-libs/multidict/issues/1131
multidict>=6.4.2
# rpds-py > 0.25.0 requires cargo 1.84.0
# Stable Alpine current only ships cargo 1.83.0
# rpds-py frequently updates cargo causing build failures
# No wheels upstream available for armhf & armv7
rpds-py==0.24.0
rpds-py==0.26.0
"""
GENERATED_MESSAGE = (

View File

@@ -32,7 +32,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \
go2rtc-client==0.2.1 \
ha-ffmpeg==3.2.2 \
hassil==2.2.3 \
home-assistant-intents==2025.6.23 \
home-assistant-intents==2025.7.30 \
mutagen==1.47.0 \
pymicro-vad==1.0.1 \
pyspeex-noise==1.0.2

View File

@@ -21,7 +21,6 @@
'aa:bb:cc:dd:ee:ff',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Acaia',
@@ -31,7 +30,6 @@
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': 'Kitchen',
'sw_version': None,
'via_device_id': None,
})

View File

@@ -21,7 +21,6 @@
'84fce612f5b8',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'AirGradient',
@@ -31,7 +30,6 @@
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': '84fce612f5b8',
'suggested_area': None,
'sw_version': '3.1.1',
'via_device_id': None,
})
@@ -58,7 +56,6 @@
'84fce612f5b8',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'AirGradient',
@@ -68,7 +65,6 @@
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': '84fce612f5b8',
'suggested_area': None,
'sw_version': '3.1.1',
'via_device_id': None,
})

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