mirror of
https://github.com/home-assistant/core.git
synced 2026-01-01 03:31:58 +01:00
Compare commits
78 Commits
2025.8.0b0
...
trigger_de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6caa86ab3 | ||
|
|
4318e29ce8 | ||
|
|
fea5c63bba | ||
|
|
b2349ac2bd | ||
|
|
08f7b708a4 | ||
|
|
1236801b7d | ||
|
|
72d9dbf39d | ||
|
|
755864f9f3 | ||
|
|
fa476d4e34 | ||
|
|
018197e41a | ||
|
|
7dd2b9e422 | ||
|
|
3e615fd373 | ||
|
|
c0bf167e10 | ||
|
|
45f6778ff4 | ||
|
|
bddd4d621a | ||
|
|
b0e75e9ee4 | ||
|
|
d45c03a795 | ||
|
|
8562c8d32f | ||
|
|
ae42d71123 | ||
|
|
9616c8cd7b | ||
|
|
9394546668 | ||
|
|
d43f21c2e2 | ||
|
|
8d68fee9f8 | ||
|
|
b4a4e218ec | ||
|
|
fb2d62d692 | ||
|
|
f538807d6e | ||
|
|
a08c3c9f44 | ||
|
|
506431c75f | ||
|
|
37579440e6 | ||
|
|
5ce2729dc2 | ||
|
|
b5e4ae4a53 | ||
|
|
3d4386ea6d | ||
|
|
9f1cec893e | ||
|
|
bc87140a6f | ||
|
|
d77a3fca83 | ||
|
|
924a86dfb6 | ||
|
|
0d7608f7c5 | ||
|
|
22e054f4cd | ||
|
|
8b53b26333 | ||
|
|
4d59e8cd80 | ||
|
|
61396d92a5 | ||
|
|
c72c600de4 | ||
|
|
b86b0c10bd | ||
|
|
eb222f6c5d | ||
|
|
4b5fe424ed | ||
|
|
61ca42e923 | ||
|
|
21c1427abf | ||
|
|
aa6b37bc7c | ||
|
|
bbc1466cfc | ||
|
|
21a9799060 | ||
|
|
f7d54b46ec | ||
|
|
6ad1b8dcb1 | ||
|
|
5f6b1212a3 | ||
|
|
58dc6a952e | ||
|
|
59d8df142d | ||
|
|
04fb86b4ba | ||
|
|
3d744f032f | ||
|
|
f7c8cdb3a7 | ||
|
|
3952544822 | ||
|
|
42101dd432 | ||
|
|
f7eacaa48d | ||
|
|
ad0db5c83a | ||
|
|
63216b77c2 | ||
|
|
7a55373b0b | ||
|
|
f9e7459901 | ||
|
|
94dc2e2ea3 | ||
|
|
2cf144fb25 | ||
|
|
f318766021 | ||
|
|
ec7fb140ac | ||
|
|
2706c7d67d | ||
|
|
b4e50902eb | ||
|
|
1ead01bc9a | ||
|
|
389a1251a1 | ||
|
|
8d27ca1e21 | ||
|
|
a76af50c10 | ||
|
|
09b91bd76a | ||
|
|
736d582d04 | ||
|
|
8114df4219 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/wheels.yml
vendored
4
.github/workflows/wheels.yml
vendored
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
33
homeassistant/components/airos/diagnostics.py
Normal file
33
homeassistant/components/airos/diagnostics.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Diagnostics support for airOS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AirOSConfigEntry
|
||||
|
||||
IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related
|
||||
HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address
|
||||
TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD]
|
||||
TO_REDACT_AIROS = [
|
||||
"hostname", # Prevent leaking device naming
|
||||
"essid", # Network SSID
|
||||
"lat", # GPS latitude to prevent exposing location data.
|
||||
"lon", # GPS longitude to prevent exposing location data.
|
||||
*HW_REDACT,
|
||||
*IP_REDACT,
|
||||
]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AirOSConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {
|
||||
"entry_data": async_redact_data(entry.data, TO_REDACT_HA),
|
||||
"data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS),
|
||||
}
|
||||
@@ -41,7 +41,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: done
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.23"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.7.30"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["denonavr"],
|
||||
"requirements": ["denonavr==1.1.1"],
|
||||
"requirements": ["denonavr==1.1.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Denon",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -316,10 +316,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# Don't call _fetch_device_info() for ignored entries
|
||||
raise AbortFlow("already_configured")
|
||||
configured_host: str | None = entry.data.get(CONF_HOST)
|
||||
configured_port: int | None = entry.data.get(CONF_PORT)
|
||||
if configured_host == host and configured_port == port:
|
||||
configured_port: int = entry.data.get(CONF_PORT, DEFAULT_PORT)
|
||||
# When port is None (from DHCP discovery), only compare hosts
|
||||
if configured_host == host and (port is None or configured_port == port):
|
||||
# Don't probe to verify the mac is correct since
|
||||
# the host and port matches.
|
||||
# the host matches (and port matches if provided).
|
||||
raise AbortFlow("already_configured")
|
||||
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
|
||||
await self._fetch_device_info(host, port or configured_port, configured_psk)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==37.1.5",
|
||||
"aioesphomeapi==37.2.2",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.1.0"
|
||||
],
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -12,6 +12,7 @@ from ha_silabs_firmware_client import (
|
||||
ManifestMissing,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -24,13 +25,20 @@ FIRMWARE_REFRESH_INTERVAL = timedelta(hours=8)
|
||||
class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]):
|
||||
"""Coordinator to manage firmware updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, session: ClientSession, url: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
session: ClientSession,
|
||||
url: str,
|
||||
) -> None:
|
||||
"""Initialize the firmware update coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="firmware update coordinator",
|
||||
update_interval=FIRMWARE_REFRESH_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
self.hass = hass
|
||||
self.session = session
|
||||
|
||||
@@ -124,6 +124,7 @@ def _async_create_update_entity(
|
||||
config_entry=config_entry,
|
||||
update_coordinator=FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
config_entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
),
|
||||
|
||||
@@ -129,6 +129,7 @@ def _async_create_update_entity(
|
||||
config_entry=config_entry,
|
||||
update_coordinator=FirmwareUpdateCoordinator(
|
||||
hass,
|
||||
config_entry,
|
||||
session,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -99,7 +99,7 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
),
|
||||
}
|
||||
)
|
||||
self.add_suggested_values_to_schema(
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
data_schema,
|
||||
{"section_1": {"int": self.config_entry.options.get(CONF_INT, 10)}},
|
||||
)
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pylitterbot==2024.2.2"]
|
||||
"requirements": ["pylitterbot==2024.2.3"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiomealie==0.10.0"]
|
||||
"requirements": ["aiomealie==0.10.1"]
|
||||
}
|
||||
|
||||
@@ -174,7 +174,8 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
||||
if list_item.display.strip() != stripped_item_summary:
|
||||
update_shopping_item.note = stripped_item_summary
|
||||
update_shopping_item.position = position
|
||||
update_shopping_item.is_food = False
|
||||
if update_shopping_item.is_food is not None:
|
||||
update_shopping_item.is_food = False
|
||||
update_shopping_item.food_id = None
|
||||
update_shopping_item.quantity = 0.0
|
||||
update_shopping_item.checked = item.status == TodoItemStatus.COMPLETED
|
||||
@@ -249,7 +250,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
||||
mutate_shopping_item.note = item.note
|
||||
mutate_shopping_item.checked = item.checked
|
||||
|
||||
if item.is_food:
|
||||
if item.is_food or item.food_id:
|
||||
mutate_shopping_item.food_id = item.food_id
|
||||
mutate_shopping_item.unit_id = item.unit_id
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> boo
|
||||
) from err
|
||||
|
||||
# Setup MieleAPI and coordinator for data fetch
|
||||
coordinator = MieleDataUpdateCoordinator(hass, auth)
|
||||
coordinator = MieleDataUpdateCoordinator(hass, entry, auth)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
|
||||
@@ -431,6 +431,16 @@ DISHWASHER_PROGRAM_ID: dict[int, str] = {
|
||||
38: "quick_power_wash",
|
||||
42: "tall_items",
|
||||
44: "power_wash",
|
||||
200: "eco",
|
||||
202: "automatic",
|
||||
203: "comfort_wash",
|
||||
204: "power_wash",
|
||||
205: "intensive",
|
||||
207: "extra_quiet",
|
||||
209: "comfort_wash_plus",
|
||||
210: "gentle",
|
||||
214: "maintenance",
|
||||
215: "rinse_salt",
|
||||
}
|
||||
TUMBLE_DRYER_PROGRAM_ID: dict[int, str] = {
|
||||
-1: "no_program", # Extrapolated from other device types.
|
||||
|
||||
@@ -42,12 +42,14 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MieleConfigEntry,
|
||||
api: AsyncConfigEntryAuth,
|
||||
) -> None:
|
||||
"""Initialize the Miele data coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=120),
|
||||
)
|
||||
|
||||
@@ -731,7 +731,7 @@ class MielePlateSensor(MieleSensor):
|
||||
)
|
||||
).name
|
||||
if self.device.state_plate_step
|
||||
else PlatePowerStep.plate_step_0
|
||||
else PlatePowerStep.plate_step_0.name
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse:
|
||||
else {}
|
||||
),
|
||||
}
|
||||
if item["parameters"]
|
||||
if item.get("parameters")
|
||||
else {}
|
||||
),
|
||||
}
|
||||
|
||||
@@ -203,27 +203,27 @@
|
||||
"plate": {
|
||||
"name": "Plate {plate_no}",
|
||||
"state": {
|
||||
"power_step_0": "0",
|
||||
"power_step_warm": "Warming",
|
||||
"power_step_1": "1",
|
||||
"power_step_2": "1\u2022",
|
||||
"power_step_3": "2",
|
||||
"power_step_4": "2\u2022",
|
||||
"power_step_5": "3",
|
||||
"power_step_6": "3\u2022",
|
||||
"power_step_7": "4",
|
||||
"power_step_8": "4\u2022",
|
||||
"power_step_9": "5",
|
||||
"power_step_10": "5\u2022",
|
||||
"power_step_11": "6",
|
||||
"power_step_12": "6\u2022",
|
||||
"power_step_13": "7",
|
||||
"power_step_14": "7\u2022",
|
||||
"power_step_15": "8",
|
||||
"power_step_16": "8\u2022",
|
||||
"power_step_17": "9",
|
||||
"power_step_18": "9\u2022",
|
||||
"power_step_boost": "Boost"
|
||||
"plate_step_0": "0",
|
||||
"plate_step_warm": "Warming",
|
||||
"plate_step_1": "1",
|
||||
"plate_step_2": "1\u2022",
|
||||
"plate_step_3": "2",
|
||||
"plate_step_4": "2\u2022",
|
||||
"plate_step_5": "3",
|
||||
"plate_step_6": "3\u2022",
|
||||
"plate_step_7": "4",
|
||||
"plate_step_8": "4\u2022",
|
||||
"plate_step_9": "5",
|
||||
"plate_step_10": "5\u2022",
|
||||
"plate_step_11": "6",
|
||||
"plate_step_12": "6\u2022",
|
||||
"plate_step_13": "7",
|
||||
"plate_step_14": "7\u2022",
|
||||
"plate_step_15": "8",
|
||||
"plate_step_16": "8\u2022",
|
||||
"plate_step_17": "9",
|
||||
"plate_step_18": "9\u2022",
|
||||
"plate_step_boost": "Boost"
|
||||
}
|
||||
},
|
||||
"drying_step": {
|
||||
@@ -485,6 +485,8 @@
|
||||
"cook_bacon": "Cook bacon",
|
||||
"biscuits_short_crust_pastry_1_tray": "Biscuits, short crust pastry (1 tray)",
|
||||
"biscuits_short_crust_pastry_2_trays": "Biscuits, short crust pastry (2 trays)",
|
||||
"comfort_wash": "Comfort wash",
|
||||
"comfort_wash_plus": "Comfort wash plus",
|
||||
"cool_air": "Cool air",
|
||||
"corn_on_the_cob": "Corn on the cob",
|
||||
"cottons": "Cottons",
|
||||
@@ -827,6 +829,7 @@
|
||||
"rice_pudding_steam_cooking": "Rice pudding (steam cooking)",
|
||||
"rinse": "Rinse",
|
||||
"rinse_out_lint": "Rinse out lint",
|
||||
"rinse_salt": "Rinse salt",
|
||||
"risotto": "Risotto",
|
||||
"ristretto": "Ristretto",
|
||||
"roast_beef_low_temperature_cooking": "Roast beef (low temperature cooking)",
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["motionblinds"],
|
||||
"requirements": ["motionblinds==0.6.29"]
|
||||
"requirements": ["motionblinds==0.6.30"]
|
||||
}
|
||||
|
||||
@@ -426,7 +426,7 @@
|
||||
},
|
||||
"data_description": {
|
||||
"payload_off": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]",
|
||||
"payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]",
|
||||
"payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_on%]",
|
||||
"power_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the power command topic. The `value` parameter is the payload set for payload \"on\" or payload \"off\".",
|
||||
"power_command_topic": "The MQTT topic to publish commands to change the climate power state. Sends the payload configured with payload \"on\" or payload \"off\". [Learn more.]({url}#power_command_topic)"
|
||||
}
|
||||
@@ -802,17 +802,17 @@
|
||||
"data": {
|
||||
"max_humidity": "Maximum humidity",
|
||||
"min_humidity": "Minimum humidity",
|
||||
"target_humidity_command_template": "Humidity command template",
|
||||
"target_humidity_command_topic": "Humidity command topic",
|
||||
"target_humidity_state_template": "Humidity state template",
|
||||
"target_humidity_state_topic": "Humidity state topic"
|
||||
"target_humidity_command_template": "Target humidity command template",
|
||||
"target_humidity_command_topic": "Target humidity command topic",
|
||||
"target_humidity_state_template": "Target humidity state template",
|
||||
"target_humidity_state_topic": "Target humidity state topic"
|
||||
},
|
||||
"data_description": {
|
||||
"max_humidity": "The maximum target humidity that can be set.",
|
||||
"min_humidity": "The minimum target humidity that can be set.",
|
||||
"target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the humidity command topic.",
|
||||
"target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the target humidity command topic.",
|
||||
"target_humidity_command_topic": "The MQTT topic to publish commands to change the climate target humidity. [Learn more.]({url}#humidity_command_topic)",
|
||||
"target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the humidity state topic with.",
|
||||
"target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the target humidity state topic with.",
|
||||
"target_humidity_state_topic": "The MQTT topic to subscribe for changes of the target humidity. [Learn more.]({url}#humidity_state_topic)"
|
||||
}
|
||||
},
|
||||
@@ -838,7 +838,7 @@
|
||||
"temperature_low_state_topic": "Lower temperature state topic"
|
||||
},
|
||||
"data_description": {
|
||||
"initial": "The climate initalizes with this target temperature.",
|
||||
"initial": "The climate initializes with this target temperature.",
|
||||
"max_temp": "The maximum target temperature that can be set.",
|
||||
"min_temp": "The minimum target temperature that can be set.",
|
||||
"precision": "The precision in degrees the thermostat is working at.",
|
||||
@@ -1104,6 +1104,7 @@
|
||||
},
|
||||
"device_class_sensor": {
|
||||
"options": {
|
||||
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
|
||||
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
||||
"area": "[%key:component::sensor::entity_component::area::name%]",
|
||||
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -158,6 +158,9 @@
|
||||
"notify": {
|
||||
"group_message": {
|
||||
"name": "Group: {group_name}"
|
||||
},
|
||||
"direct_message": {
|
||||
"name": "Direct message"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -10,6 +10,7 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
Platform.SCENE,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
378
homeassistant/components/qbus/sensor.py
Normal file
378
homeassistant/components/qbus/sensor.py
Normal 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
|
||||
@@ -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}."
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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%]",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pysqueezebox import Player
|
||||
@@ -21,6 +22,8 @@ from homeassistant.helpers.network import is_internal_request
|
||||
|
||||
from .const import DOMAIN, UNPLAYABLE_TYPES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LIBRARY = [
|
||||
"favorites",
|
||||
"artists",
|
||||
@@ -138,18 +141,42 @@ class BrowseData:
|
||||
self.squeezebox_id_by_type.update(SQUEEZEBOX_ID_BY_TYPE)
|
||||
self.media_type_to_squeezebox.update(MEDIA_TYPE_TO_SQUEEZEBOX)
|
||||
|
||||
def add_new_command(self, cmd: str | MediaType, type: str) -> None:
|
||||
"""Add items to maps for new apps or radios."""
|
||||
self.known_apps_radios.add(cmd)
|
||||
self.media_type_to_squeezebox[cmd] = cmd
|
||||
self.squeezebox_id_by_type[cmd] = type
|
||||
self.content_type_media_class[cmd] = {
|
||||
"item": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.TRACK,
|
||||
}
|
||||
self.content_type_to_child_type[cmd] = MediaType.TRACK
|
||||
|
||||
def _add_new_command_to_browse_data(
|
||||
browse_data: BrowseData, cmd: str | MediaType, type: str
|
||||
) -> None:
|
||||
"""Add items to maps for new apps or radios."""
|
||||
browse_data.media_type_to_squeezebox[cmd] = cmd
|
||||
browse_data.squeezebox_id_by_type[cmd] = type
|
||||
browse_data.content_type_media_class[cmd] = {
|
||||
"item": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.TRACK,
|
||||
}
|
||||
browse_data.content_type_to_child_type[cmd] = MediaType.TRACK
|
||||
async def async_init(self, player: Player, browse_limit: int) -> None:
|
||||
"""Initialize known apps and radios from the player."""
|
||||
|
||||
cmd = ["apps", 0, browse_limit]
|
||||
result = await player.async_query(*cmd)
|
||||
for app in result["appss_loop"]:
|
||||
app_cmd = "app-" + app["cmd"]
|
||||
if app_cmd not in self.known_apps_radios:
|
||||
self.add_new_command(app_cmd, "item_id")
|
||||
_LOGGER.debug(
|
||||
"Adding new command %s to browse data for player %s",
|
||||
app_cmd,
|
||||
player.player_id,
|
||||
)
|
||||
cmd = ["radios", 0, browse_limit]
|
||||
result = await player.async_query(*cmd)
|
||||
for app in result["radioss_loop"]:
|
||||
app_cmd = "app-" + app["cmd"]
|
||||
if app_cmd not in self.known_apps_radios:
|
||||
self.add_new_command(app_cmd, "item_id")
|
||||
_LOGGER.debug(
|
||||
"Adding new command %s to browse data for player %s",
|
||||
app_cmd,
|
||||
player.player_id,
|
||||
)
|
||||
|
||||
|
||||
def _build_response_apps_radios_category(
|
||||
@@ -292,8 +319,7 @@ async def build_item_response(
|
||||
app_cmd = "app-" + item["cmd"]
|
||||
|
||||
if app_cmd not in browse_data.known_apps_radios:
|
||||
browse_data.known_apps_radios.add(app_cmd)
|
||||
_add_new_command_to_browse_data(browse_data, app_cmd, "item_id")
|
||||
browse_data.add_new_command(app_cmd, "item_id")
|
||||
|
||||
child_media = _build_response_apps_radios_category(
|
||||
browse_data=browse_data, cmd=app_cmd, item=item
|
||||
|
||||
@@ -311,6 +311,11 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
)
|
||||
return None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await self._browse_data.async_init(self._player, self.browse_limit)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove from list of known players when removed from hass."""
|
||||
self.coordinator.config_entry.runtime_data.known_player_ids.remove(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"common": {
|
||||
"advanced_options": "Advanced options",
|
||||
"availability": "Availability template",
|
||||
"availability_description": "Defines a template to get the `available` state of the entity. If the template either fails to render or returns `True`, `\"1\"`, `\"true\"`, `\"yes\"`, `\"on\"`, `\"enable\"`, or a non-zero number, the entity will be `available`. If the template returns any other value, the entity will be `unavailable`. If not configured, the entity will always be `available`. Note that the string comparison is not case sensitive; `\"TrUe\"` and `\"yEs\"` are allowed.",
|
||||
"code_format": "Code format",
|
||||
"device_class": "Device class",
|
||||
"device_id_description": "Select a device to link to this entity.",
|
||||
@@ -28,13 +29,26 @@
|
||||
"code_format": "[%key:component::template::common::code_format%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"value_template": "Defines a template to set the state of the alarm panel. Valid output values from the template are `armed_away`, `armed_home`, `armed_night`, `armed_vacation`, `arming`, `disarmed`, `pending`, and `triggered`.",
|
||||
"disarm": "Defines actions to run when the alarm control panel is disarmed. Receives variable `code`.",
|
||||
"arm_away": "Defines actions to run when the alarm control panel is armed to `arm_away`. Receives variable `code`.",
|
||||
"arm_custom_bypass": "Defines actions to run when the alarm control panel is armed to `arm_custom_bypass`. Receives variable `code`.",
|
||||
"arm_home": "Defines actions to run when the alarm control panel is armed to `arm_home`. Receives variable `code`.",
|
||||
"arm_night": "Defines actions to run when the alarm control panel is armed to `arm_night`. Receives variable `code`.",
|
||||
"arm_vacation": "Defines actions to run when the alarm control panel is armed to `arm_vacation`. Receives variable `code`.",
|
||||
"trigger": "Defines actions to run when the alarm control panel is triggered. Receives variable `code`.",
|
||||
"code_arm_required": "If true, the code is required to arm the alarm.",
|
||||
"code_format": "One of `number`, `text` or `no_code`. Format for the code used to arm/disarm the alarm."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -48,13 +62,17 @@
|
||||
"state": "[%key:component::template::common::state%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "The sensor is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68,13 +86,17 @@
|
||||
"press": "Actions on press"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"press": "Defines actions to run when button is pressed."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -99,13 +121,16 @@
|
||||
"close_cover": "Defines actions to run when the cover is closed.",
|
||||
"stop_cover": "Defines actions to run when the cover is stopped.",
|
||||
"position": "Defines a template to get the position of the cover. Value values are numbers between `0` (`closed`) and `100` (`open`).",
|
||||
"set_cover_position": "Defines actions to run when the cover is given a `set_cover_position` command."
|
||||
"set_cover_position": "Defines actions to run when the cover is given a `set_cover_position` command. Receives variable `position`."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -124,11 +149,11 @@
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "Defines a template to get the state of the fan. Valid values: `on`, `off`.",
|
||||
"state": "The fan is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`.",
|
||||
"turn_off": "Defines actions to run when the fan is turned off.",
|
||||
"turn_on": "Defines actions to run when the fan is turned on.",
|
||||
"turn_on": "Defines actions to run when the fan is turned on. Receives variables `percentage` and/or `preset_mode`.",
|
||||
"percentage": "Defines a template to get the speed percentage of the fan.",
|
||||
"set_percentage": "Defines actions to run when the fan is given a speed percentage command.",
|
||||
"set_percentage": "Defines actions to run when the fan is given a speed percentage command. Receives variable `percentage`.",
|
||||
"speed_count": "The number of speeds the fan supports. Used to calculate the percentage step for the `fan.increase_speed` and `fan.decrease_speed` actions."
|
||||
},
|
||||
"sections": {
|
||||
@@ -136,6 +161,9 @@
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -149,13 +177,18 @@
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"url": "Defines a template to get the URL on which the image is served.",
|
||||
"verify_ssl": "Enable or disable SSL certificate verification. Disable to use an http URL, or if you have a self-signed SSL certificate and haven’t installed the CA certificate to enable verification."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -176,13 +209,25 @@
|
||||
"set_temperature": "Actions on set color temperature"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "The light is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`.",
|
||||
"turn_off": "Defines actions to run when the light is turned off.",
|
||||
"turn_on": "Defines actions to run when the light is turned on.",
|
||||
"level": "Defines a template to get the brightness of the light. Valid values are 0 to 255.",
|
||||
"set_level": "Defines actions to run when the light is given a brightness command. The script will only be called if the `turn_on` call only has `brightness`, and optionally `transition`. Receives variables `brightness` and, optionally, `transition`.",
|
||||
"hs": "Defines a template to get the HS color of the light. Must render a tuple (hue, saturation).",
|
||||
"set_hs": "Defines actions to run when the light is given a hs color command. Available variables: `hs` as a tuple, `h` and `s`.",
|
||||
"temperature": "Defines a template to get the color temperature of the light.",
|
||||
"set_temperature": "Defines actions to run when the light is given a color temperature command. Receives variable `color_temp_kelvin`. May also receive variables `brightness` and/or `transition`."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -199,13 +244,21 @@
|
||||
"open": "Actions on open"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "Defines a template to set the state of the lock. The lock is locked if the template evaluates to `True`, `true`, `on`, or `locked`. The lock is unlocked if the template evaluates to `False`, `false`, `off`, or `unlocked`. Other valid states are `jammed`, `opening`, `locking`, `open`, and `unlocking`.",
|
||||
"lock": "Defines actions to run when the lock is locked.",
|
||||
"unlock": "Defines actions to run when the lock is unlocked.",
|
||||
"code_format": "Defines a template to get the `code_format` attribute of the lock.",
|
||||
"open": "Defines actions to run when the lock is opened."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -223,13 +276,22 @@
|
||||
"unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "Template for the number's current value.",
|
||||
"step": "Defines the number's increment/decrement step.",
|
||||
"set_value": "Defines actions to run when the number is set to a value. Receives variable `value`.",
|
||||
"max": "Defines the number's maximum value.",
|
||||
"min": "Defines the number's minimum value.",
|
||||
"unit_of_measurement": "Defines the unit of measurement of the number, if any."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -244,13 +306,19 @@
|
||||
"options": "Available options"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "Template for the select’s current value.",
|
||||
"select_option": "Defines actions to run when an `option` from the `options` list is selected. Receives variable `option`.",
|
||||
"options": "Template for the select’s available options."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -266,13 +334,18 @@
|
||||
"unit_of_measurement": "[%key:component::template::common::unit_of_measurement%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "Defines a template to get the state of the sensor. If the sensor is numeric, i.e. it has a `state_class` or a `unit_of_measurement`, the state template must render to a number or to `none`. The state template must not render to a string, including `unknown` or `unavailable`. An `availability` template may be defined to suppress rendering of the state template.",
|
||||
"unit_of_measurement": "Defines the unit of measurement for the sensor, if any. This will also display the value based on the number format setting in the user profile and influence the graphical presentation in the history visualization as a continuous value."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -307,13 +380,18 @@
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful."
|
||||
"value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful.",
|
||||
"turn_off": "Defines actions to run when the switch is turned off.",
|
||||
"turn_on": "Defines actions to run when the switch is turned on."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -324,24 +402,37 @@
|
||||
"device_id": "[%key:common::config_flow::data::device%]",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"state": "[%key:component::template::common::state%]",
|
||||
"start": "Actions on turn off",
|
||||
"start": "Actions on start",
|
||||
"fan_speed": "Fan speed",
|
||||
"fan_speeds": "Fan speeds",
|
||||
"set_fan_speed": "Actions on set fan speed",
|
||||
"stop": "Actions on stop",
|
||||
"pause": "Actions on pause",
|
||||
"return_to_base": "Actions on return to base",
|
||||
"return_to_base": "Actions on return to dock",
|
||||
"clean_spot": "Actions on clean spot",
|
||||
"locate": "Actions on locate"
|
||||
},
|
||||
"data_description": {
|
||||
"device_id": "[%key:component::template::common::device_id_description%]"
|
||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||
"state": "Defines a template to get the state of the vacuum. Valid values are `cleaning`, `docked`, `idle`, `paused`, `returning`, and `error`.",
|
||||
"start": "Defines actions to run when the vacuum is started.",
|
||||
"fan_speed": "Defines a template to get the fan speed of the vacuum.",
|
||||
"fan_speeds": "List of fan speeds supported by the vacuum.",
|
||||
"set_fan_speed": "Defines actions to run when the vacuum is given a command to set the fan speed. Receives variable `fan_speed`.",
|
||||
"stop": "Defines actions to run when the vacuum is stopped.",
|
||||
"pause": "Defines actions to run when the vacuum is paused.",
|
||||
"return_to_base": "Defines actions to run when the vacuum is given a 'Return to dock' command.",
|
||||
"clean_spot": "Defines actions to run when the vacuum is given a 'Clean spot' command.",
|
||||
"locate": "Defines actions to run when the vacuum is given a 'Locate' command."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"name": "[%key:component::template::common::advanced_options%]",
|
||||
"data": {
|
||||
"availability": "[%key:component::template::common::availability%]"
|
||||
},
|
||||
"data_description": {
|
||||
"availability": "[%key:component::template::common::availability_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -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%]",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -162,7 +162,11 @@ class UptimeKumaSensorEntity(
|
||||
name=coordinator.data[monitor].monitor_name,
|
||||
identifiers={(DOMAIN, f"{coordinator.config_entry.entry_id}_{monitor!s}")},
|
||||
manufacturer="Uptime Kuma",
|
||||
configuration_url=coordinator.config_entry.data[CONF_URL],
|
||||
configuration_url=(
|
||||
None
|
||||
if "127.0.0.1" in (url := coordinator.config_entry.data[CONF_URL])
|
||||
else url
|
||||
),
|
||||
sw_version=coordinator.api.version.version,
|
||||
)
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["voip_utils"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["voip-utils==0.3.3"]
|
||||
"requirements": ["voip-utils==0.3.4"]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
from volvocarsapi.api import VolvoCarsApi
|
||||
from volvocarsapi.models import VolvoApiException, VolvoCarsVehicle
|
||||
from volvocarsapi.scopes import DEFAULT_SCOPES
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
@@ -54,6 +55,13 @@ class VolvoOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
self._vehicles: list[VolvoCarsVehicle] = []
|
||||
self._config_data: dict = {}
|
||||
|
||||
@property
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
return super().extra_authorize_data | {
|
||||
"scope": " ".join(DEFAULT_SCOPES),
|
||||
}
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -74,7 +74,12 @@ from zha.event import EventBase
|
||||
from zha.exceptions import ZHAException
|
||||
from zha.mixins import LogMixin
|
||||
from zha.zigbee.cluster_handlers import ClusterBindEvent, ClusterConfigureReportingEvent
|
||||
from zha.zigbee.device import ClusterHandlerConfigurationComplete, Device, ZHAEvent
|
||||
from zha.zigbee.device import (
|
||||
ClusterHandlerConfigurationComplete,
|
||||
Device,
|
||||
DeviceFirmwareInfoUpdatedEvent,
|
||||
ZHAEvent,
|
||||
)
|
||||
from zha.zigbee.group import Group, GroupInfo, GroupMember
|
||||
from zigpy.config import (
|
||||
CONF_DATABASE,
|
||||
@@ -843,8 +848,23 @@ class ZHAGatewayProxy(EventBase):
|
||||
name=zha_device.name,
|
||||
manufacturer=zha_device.manufacturer,
|
||||
model=zha_device.model,
|
||||
sw_version=zha_device.firmware_version,
|
||||
)
|
||||
zha_device_proxy.device_id = device_registry_device.id
|
||||
|
||||
def update_sw_version(event: DeviceFirmwareInfoUpdatedEvent) -> None:
|
||||
"""Update software version in device registry."""
|
||||
device_registry.async_update_device(
|
||||
device_registry_device.id,
|
||||
sw_version=event.new_firmware_version,
|
||||
)
|
||||
|
||||
self._unsubs.append(
|
||||
zha_device.on_event(
|
||||
DeviceFirmwareInfoUpdatedEvent.event_type, update_sw_version
|
||||
)
|
||||
)
|
||||
|
||||
return zha_device_proxy
|
||||
|
||||
def _async_get_or_create_group_proxy(self, group_info: GroupInfo) -> ZHAGroupProxy:
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"zha",
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": ["zha==0.0.62"],
|
||||
"requirements": ["zha==0.0.64"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10C4",
|
||||
|
||||
@@ -616,6 +616,18 @@
|
||||
},
|
||||
"water_supply": {
|
||||
"name": "Water supply"
|
||||
},
|
||||
"frient_in_1": {
|
||||
"name": "IN1"
|
||||
},
|
||||
"frient_in_2": {
|
||||
"name": "IN2"
|
||||
},
|
||||
"frient_in_3": {
|
||||
"name": "IN3"
|
||||
},
|
||||
"frient_in_4": {
|
||||
"name": "IN4"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@@ -639,6 +651,9 @@
|
||||
},
|
||||
"frost_lock_reset": {
|
||||
"name": "Frost lock reset"
|
||||
},
|
||||
"reset_alarm": {
|
||||
"name": "Reset alarm"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
@@ -1472,6 +1487,9 @@
|
||||
"tier6_summation_delivered": {
|
||||
"name": "Tier 6 summation delivered"
|
||||
},
|
||||
"total_active_power": {
|
||||
"name": "Total power"
|
||||
},
|
||||
"summation_received": {
|
||||
"name": "Summation received"
|
||||
},
|
||||
@@ -2006,6 +2024,18 @@
|
||||
},
|
||||
"auto_relock": {
|
||||
"name": "Autorelock"
|
||||
},
|
||||
"distance_tracking": {
|
||||
"name": "Distance tracking"
|
||||
},
|
||||
"water_shortage_auto_close": {
|
||||
"name": "Water shortage auto-close"
|
||||
},
|
||||
"frient_com_1": {
|
||||
"name": "COM 1"
|
||||
},
|
||||
"frient_com_2": {
|
||||
"name": "COM 2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ async def async_setup_entry(
|
||||
zha_data = get_zha_data(hass)
|
||||
if zha_data.update_coordinator is None:
|
||||
zha_data.update_coordinator = ZHAFirmwareUpdateCoordinator(
|
||||
hass, get_zha_gateway(hass).application_controller
|
||||
hass, config_entry, get_zha_gateway(hass).application_controller
|
||||
)
|
||||
entities_to_create = zha_data.platforms[Platform.UPDATE]
|
||||
|
||||
@@ -79,12 +79,16 @@ class ZHAFirmwareUpdateCoordinator(DataUpdateCoordinator[None]): # pylint: disa
|
||||
"""Firmware update coordinator that broadcasts updates network-wide."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, controller_application: ControllerApplication
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
controller_application: ControllerApplication,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name="ZHA firmware update coordinator",
|
||||
update_method=self.async_update_data,
|
||||
)
|
||||
|
||||
@@ -105,7 +105,6 @@ from .const import (
|
||||
CONF_USB_PATH,
|
||||
CONF_USE_ADDON,
|
||||
DOMAIN,
|
||||
DRIVER_READY_TIMEOUT,
|
||||
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
||||
EVENT_VALUE_UPDATED,
|
||||
LIB_LOGGER,
|
||||
@@ -136,6 +135,7 @@ from .models import ZwaveJSConfigEntry, ZwaveJSData
|
||||
from .services import async_setup_services
|
||||
|
||||
CONNECT_TIMEOUT = 10
|
||||
DRIVER_READY_TIMEOUT = 60
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -368,6 +368,16 @@ class DriverEvents:
|
||||
)
|
||||
)
|
||||
|
||||
# listen for driver ready event to reload the config entry
|
||||
self.config_entry.async_on_unload(
|
||||
driver.on(
|
||||
"driver ready",
|
||||
lambda _: self.hass.config_entries.async_schedule_reload(
|
||||
self.config_entry.entry_id
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# listen for new nodes being added to the mesh
|
||||
self.config_entry.async_on_unload(
|
||||
controller.on(
|
||||
@@ -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:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
from contextlib import suppress
|
||||
import dataclasses
|
||||
@@ -87,7 +86,6 @@ from .const import (
|
||||
CONF_DATA_COLLECTION_OPTED_IN,
|
||||
CONF_INSTALLER_MODE,
|
||||
DOMAIN,
|
||||
DRIVER_READY_TIMEOUT,
|
||||
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
||||
LOGGER,
|
||||
USER_AGENT,
|
||||
@@ -98,6 +96,7 @@ from .helpers import (
|
||||
async_get_node_from_device_id,
|
||||
async_get_provisioning_entry_from_device_id,
|
||||
async_get_version_info,
|
||||
async_wait_for_driver_ready_event,
|
||||
get_device_id,
|
||||
)
|
||||
|
||||
@@ -2854,26 +2853,18 @@ async def websocket_hard_reset_controller(
|
||||
connection.send_result(msg[ID], device.id)
|
||||
async_cleanup()
|
||||
|
||||
@callback
|
||||
def set_driver_ready(event: dict) -> None:
|
||||
"Set the driver ready event."
|
||||
wait_driver_ready.set()
|
||||
|
||||
wait_driver_ready = asyncio.Event()
|
||||
|
||||
msg[DATA_UNSUBSCRIBE] = unsubs = [
|
||||
async_dispatcher_connect(
|
||||
hass, EVENT_DEVICE_ADDED_TO_REGISTRY, _handle_device_added
|
||||
),
|
||||
driver.once("driver ready", set_driver_ready),
|
||||
]
|
||||
|
||||
wait_for_driver_ready = async_wait_for_driver_ready_event(entry, driver)
|
||||
|
||||
await driver.async_hard_reset()
|
||||
|
||||
with suppress(TimeoutError):
|
||||
async with asyncio.timeout(DRIVER_READY_TIMEOUT):
|
||||
await wait_driver_ready.wait()
|
||||
|
||||
await wait_for_driver_ready()
|
||||
# When resetting the controller, the controller home id is also changed.
|
||||
# The controller state in the client is stale after resetting the controller,
|
||||
# so get the new home id with a new client using the helper function.
|
||||
@@ -2886,14 +2877,14 @@ async def websocket_hard_reset_controller(
|
||||
# The stale unique id needs to be handled by a repair flow,
|
||||
# after the config entry has been reloaded.
|
||||
LOGGER.error(
|
||||
"Failed to get server version, cannot update config entry"
|
||||
"Failed to get server version, cannot update config entry "
|
||||
"unique id with new home id, after controller reset"
|
||||
)
|
||||
else:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, unique_id=str(version_info.home_id)
|
||||
)
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
@@ -3100,27 +3091,19 @@ async def websocket_restore_nvm(
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def set_driver_ready(event: dict) -> None:
|
||||
"Set the driver ready event."
|
||||
wait_driver_ready.set()
|
||||
|
||||
wait_driver_ready = asyncio.Event()
|
||||
|
||||
# Set up subscription for progress events
|
||||
connection.subscriptions[msg["id"]] = async_cleanup
|
||||
msg[DATA_UNSUBSCRIBE] = unsubs = [
|
||||
controller.on("nvm convert progress", forward_progress),
|
||||
controller.on("nvm restore progress", forward_progress),
|
||||
driver.once("driver ready", set_driver_ready),
|
||||
]
|
||||
|
||||
wait_for_driver_ready = async_wait_for_driver_ready_event(entry, driver)
|
||||
|
||||
await controller.async_restore_nvm_base64(msg["data"], {"preserveRoutes": False})
|
||||
|
||||
with suppress(TimeoutError):
|
||||
async with asyncio.timeout(DRIVER_READY_TIMEOUT):
|
||||
await wait_driver_ready.wait()
|
||||
|
||||
await wait_for_driver_ready()
|
||||
# When restoring the NVM to the controller, the controller home id is also changed.
|
||||
# The controller state in the client is stale after restoring the NVM,
|
||||
# so get the new home id with a new client using the helper function.
|
||||
@@ -3133,14 +3116,13 @@ async def websocket_restore_nvm(
|
||||
# The stale unique id needs to be handled by a repair flow,
|
||||
# after the config entry has been reloaded.
|
||||
LOGGER.error(
|
||||
"Failed to get server version, cannot update config entry"
|
||||
"Failed to get server version, cannot update config entry "
|
||||
"unique id with new home id, after controller NVM restore"
|
||||
)
|
||||
else:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, unique_id=str(version_info.home_id)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
connection.send_message(
|
||||
@@ -3152,3 +3134,4 @@ async def websocket_restore_nvm(
|
||||
)
|
||||
)
|
||||
connection.send_result(msg[ID])
|
||||
async_cleanup()
|
||||
|
||||
@@ -62,9 +62,12 @@ from .const import (
|
||||
CONF_USB_PATH,
|
||||
CONF_USE_ADDON,
|
||||
DOMAIN,
|
||||
DRIVER_READY_TIMEOUT,
|
||||
)
|
||||
from .helpers import CannotConnect, async_get_version_info
|
||||
from .helpers import (
|
||||
CannotConnect,
|
||||
async_get_version_info,
|
||||
async_wait_for_driver_ready_event,
|
||||
)
|
||||
from .models import ZwaveJSConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -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,
|
||||
|
||||
@@ -201,7 +201,3 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = {
|
||||
WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE,
|
||||
WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION,
|
||||
}
|
||||
|
||||
# Other constants
|
||||
|
||||
DRIVER_READY_TIMEOUT = 60
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import astuple, dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
@@ -56,6 +56,7 @@ from .const import (
|
||||
)
|
||||
from .models import ZwaveJSConfigEntry
|
||||
|
||||
DRIVER_READY_EVENT_TIMEOUT = 60
|
||||
SERVER_VERSION_TIMEOUT = 10
|
||||
|
||||
|
||||
@@ -588,5 +589,57 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio
|
||||
return version_info
|
||||
|
||||
|
||||
@callback
|
||||
def async_wait_for_driver_ready_event(
|
||||
config_entry: ZwaveJSConfigEntry,
|
||||
driver: Driver,
|
||||
) -> Callable[[], Coroutine[Any, Any, None]]:
|
||||
"""Wait for the driver ready event and the config entry reload.
|
||||
|
||||
When the driver ready event is received
|
||||
the config entry will be reloaded by the integration.
|
||||
This function helps wait for that to happen
|
||||
before proceeding with further actions.
|
||||
|
||||
If the config entry is reloaded for another reason,
|
||||
this function will not wait for it to be reloaded again.
|
||||
|
||||
Raises TimeoutError if the driver ready event and reload
|
||||
is not received within the specified timeout.
|
||||
"""
|
||||
driver_ready_event_received = asyncio.Event()
|
||||
config_entry_reloaded = asyncio.Event()
|
||||
unsubscribers: list[Callable[[], None]] = []
|
||||
|
||||
@callback
|
||||
def driver_ready_received(event: dict) -> None:
|
||||
"""Receive the driver ready event."""
|
||||
driver_ready_event_received.set()
|
||||
|
||||
unsubscribers.append(driver.once("driver ready", driver_ready_received))
|
||||
|
||||
@callback
|
||||
def on_config_entry_state_change() -> None:
|
||||
"""Check config entry was loaded after driver ready event."""
|
||||
if config_entry.state is ConfigEntryState.LOADED:
|
||||
config_entry_reloaded.set()
|
||||
|
||||
unsubscribers.append(
|
||||
config_entry.async_on_state_change(on_config_entry_state_change)
|
||||
)
|
||||
|
||||
async def wait_for_events() -> None:
|
||||
try:
|
||||
async with asyncio.timeout(DRIVER_READY_EVENT_TIMEOUT):
|
||||
await asyncio.gather(
|
||||
driver_ready_event_received.wait(), config_entry_reloaded.wait()
|
||||
)
|
||||
finally:
|
||||
for unsubscribe in unsubscribers:
|
||||
unsubscribe()
|
||||
|
||||
return wait_for_events
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Indicate connection error."""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
26
requirements_all.txt
generated
@@ -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
|
||||
|
||||
26
requirements_test_all.txt
generated
26
requirements_test_all.txt
generated
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
2
script/hassfest/docker/Dockerfile
generated
2
script/hassfest/docker/Dockerfile
generated
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user