mirror of
https://github.com/home-assistant/core.git
synced 2025-11-17 15:00:12 +00:00
Compare commits
150 Commits
claude/tri
...
tibber_dat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eb40574bc | ||
|
|
4fd1ef5483 | ||
|
|
7ec5d5305d | ||
|
|
7f31d2538e | ||
|
|
e1943307cf | ||
|
|
a06529d187 | ||
|
|
a0e0549d90 | ||
|
|
ba034c6c8c | ||
|
|
008bb85c59 | ||
|
|
cf1c1294d3 | ||
|
|
21554af6a1 | ||
|
|
11d5d314cc | ||
|
|
6f0de3071a | ||
|
|
87d2597292 | ||
|
|
437bc04fe8 | ||
|
|
67a0d6a187 | ||
|
|
abb52bca81 | ||
|
|
d2d6889278 | ||
|
|
bdca592219 | ||
|
|
5c0c7b9ec3 | ||
|
|
9717599fb9 | ||
|
|
4d7de2f814 | ||
|
|
779590ce1c | ||
|
|
f3a185ff9c | ||
|
|
5a5a106984 | ||
|
|
796b421d99 | ||
|
|
0c03e8dbe9 | ||
|
|
47cf4e3ffe | ||
|
|
0ea0fc151d | ||
|
|
b7e5afec9f | ||
|
|
7a2bb67e82 | ||
|
|
e0612bec07 | ||
|
|
a06f4b6776 | ||
|
|
275670a526 | ||
|
|
d0d62526dd | ||
|
|
aefdf412b0 | ||
|
|
56ab6b2512 | ||
|
|
d1dea85cf5 | ||
|
|
84b0d39763 | ||
|
|
3aff225bc3 | ||
|
|
b4aae93c45 | ||
|
|
04458e01be | ||
|
|
ae51cfb8c0 | ||
|
|
c116a9c037 | ||
|
|
fb58758684 | ||
|
|
25fbcbc68c | ||
|
|
a670286b45 | ||
|
|
52ba55b17f | ||
|
|
ff0fc98c36 | ||
|
|
9f78a2263d | ||
|
|
9b4696a80b | ||
|
|
70fe8cae39 | ||
|
|
95eb45ab08 | ||
|
|
84f8e57141 | ||
|
|
f484b6df0d | ||
|
|
34c1d45ee0 | ||
|
|
09a105d9ad | ||
|
|
6bd1787d0a | ||
|
|
37040f5064 | ||
|
|
531397ec07 | ||
|
|
d6cc0f81de | ||
|
|
f8ef8a466a | ||
|
|
713015e26a | ||
|
|
f9c1e81c5e | ||
|
|
0549d113e6 | ||
|
|
0d842978ec | ||
|
|
1f9c244c5c | ||
|
|
55476ef6ea | ||
|
|
0e130d8fdd | ||
|
|
20bcb84956 | ||
|
|
9fa1b1b8df | ||
|
|
bbb1d57081 | ||
|
|
121406569b | ||
|
|
4866c775ce | ||
|
|
f3ac3ecf05 | ||
|
|
7c5ab12270 | ||
|
|
099edfac20 | ||
|
|
aa31df0fd5 | ||
|
|
13fbeb6cdb | ||
|
|
8d557447df | ||
|
|
e6e3f2455f | ||
|
|
c9c518ee84 | ||
|
|
214731e964 | ||
|
|
9477b2206b | ||
|
|
c4b09c9a0a | ||
|
|
f5b5b2fb70 | ||
|
|
bb3cdd382b | ||
|
|
8d09b5c273 | ||
|
|
d92fa7fa72 | ||
|
|
0c45b7f615 | ||
|
|
bfa1116115 | ||
|
|
4984237987 | ||
|
|
3839573151 | ||
|
|
e02dc53df3 | ||
|
|
bedae1e12c | ||
|
|
b4eb73be98 | ||
|
|
0ac3f776fa | ||
|
|
8e8a4fff11 | ||
|
|
579ffcc64d | ||
|
|
81943fb31d | ||
|
|
70dd0bf12e | ||
|
|
c2d462c1e7 | ||
|
|
49e050cc60 | ||
|
|
f6d829a2f3 | ||
|
|
e44e3b6f25 | ||
|
|
af603661c0 | ||
|
|
35c6113777 | ||
|
|
3c2f729ddc | ||
|
|
0d63cb765f | ||
|
|
3cb414511b | ||
|
|
f55c36d42d | ||
|
|
26bb301cc0 | ||
|
|
4159e483ee | ||
|
|
7eb6f7cc07 | ||
|
|
a7d01b0b03 | ||
|
|
1e5cfddf83 | ||
|
|
006fc5b10a | ||
|
|
35a4b685b3 | ||
|
|
b166818ef4 | ||
|
|
34cd9f11d0 | ||
|
|
0711d62085 | ||
|
|
f70aeafb5f | ||
|
|
e2279b3589 | ||
|
|
87b68e99ec | ||
|
|
b6c8b787e8 | ||
|
|
78f26edc29 | ||
|
|
5e6a72de90 | ||
|
|
dcc559f8b6 | ||
|
|
eda49cced0 | ||
|
|
14e41ab119 | ||
|
|
46151456d8 | ||
|
|
39773a022a | ||
|
|
5f49a6450f | ||
|
|
dc8425c580 | ||
|
|
910bd371e4 | ||
|
|
802a225e11 | ||
|
|
84f66fa689 | ||
|
|
0b7e88d0e0 | ||
|
|
1fcaf95df5 | ||
|
|
6c7434531f | ||
|
|
5ec1c2b68b | ||
|
|
d8636d8346 | ||
|
|
434763c74d | ||
|
|
8cd2c1b43b | ||
|
|
44711787a4 | ||
|
|
98fd0ee683 | ||
|
|
303e4ce961 | ||
|
|
76f29298cd | ||
|
|
17f5d0a69f | ||
|
|
90561de438 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CACHE_VERSION: 1
|
CACHE_VERSION: 2
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2025.12"
|
HA_SHORT_VERSION: "2025.12"
|
||||||
|
|||||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
|||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ homeassistant.components.google_cloud.*
|
|||||||
homeassistant.components.google_drive.*
|
homeassistant.components.google_drive.*
|
||||||
homeassistant.components.google_photos.*
|
homeassistant.components.google_photos.*
|
||||||
homeassistant.components.google_sheets.*
|
homeassistant.components.google_sheets.*
|
||||||
|
homeassistant.components.google_weather.*
|
||||||
homeassistant.components.govee_ble.*
|
homeassistant.components.govee_ble.*
|
||||||
homeassistant.components.gpsd.*
|
homeassistant.components.gpsd.*
|
||||||
homeassistant.components.greeneye_monitor.*
|
homeassistant.components.greeneye_monitor.*
|
||||||
|
|||||||
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -607,6 +607,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/google_tasks/ @allenporter
|
/tests/components/google_tasks/ @allenporter
|
||||||
/homeassistant/components/google_travel_time/ @eifinger
|
/homeassistant/components/google_travel_time/ @eifinger
|
||||||
/tests/components/google_travel_time/ @eifinger
|
/tests/components/google_travel_time/ @eifinger
|
||||||
|
/homeassistant/components/google_weather/ @tronikos
|
||||||
|
/tests/components/google_weather/ @tronikos
|
||||||
/homeassistant/components/govee_ble/ @bdraco
|
/homeassistant/components/govee_ble/ @bdraco
|
||||||
/tests/components/govee_ble/ @bdraco
|
/tests/components/govee_ble/ @bdraco
|
||||||
/homeassistant/components/govee_light_local/ @Galorhallen
|
/homeassistant/components/govee_light_local/ @Galorhallen
|
||||||
@@ -1374,6 +1376,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/sanix/ @tomaszsluszniak
|
/tests/components/sanix/ @tomaszsluszniak
|
||||||
/homeassistant/components/satel_integra/ @Tommatheussen
|
/homeassistant/components/satel_integra/ @Tommatheussen
|
||||||
/tests/components/satel_integra/ @Tommatheussen
|
/tests/components/satel_integra/ @Tommatheussen
|
||||||
|
/homeassistant/components/saunum/ @mettolen
|
||||||
|
/tests/components/saunum/ @mettolen
|
||||||
/homeassistant/components/scene/ @home-assistant/core
|
/homeassistant/components/scene/ @home-assistant/core
|
||||||
/tests/components/scene/ @home-assistant/core
|
/tests/components/scene/ @home-assistant/core
|
||||||
/homeassistant/components/schedule/ @home-assistant/core
|
/homeassistant/components/schedule/ @home-assistant/core
|
||||||
|
|||||||
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
|||||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.1
|
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.11.0
|
||||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.1
|
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.11.0
|
||||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.1
|
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.11.0
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.11.0
|
||||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.1
|
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.11.0
|
||||||
cosign:
|
cosign:
|
||||||
base_identity: https://github.com/home-assistant/docker/.*
|
base_identity: https://github.com/home-assistant/docker/.*
|
||||||
identity: https://github.com/home-assistant/core/.*
|
identity: https://github.com/home-assistant/core/.*
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"google_tasks",
|
"google_tasks",
|
||||||
"google_translate",
|
"google_translate",
|
||||||
"google_travel_time",
|
"google_travel_time",
|
||||||
|
"google_weather",
|
||||||
"google_wifi",
|
"google_wifi",
|
||||||
"google",
|
"google",
|
||||||
"nest",
|
"nest",
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["adguardhome"],
|
"loggers": ["adguardhome"],
|
||||||
"requirements": ["adguardhome==0.7.0"]
|
"requirements": ["adguardhome==0.8.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
||||||
@@ -43,6 +44,9 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
name=entry.title,
|
name=entry.title,
|
||||||
config_entry=entry,
|
config_entry=entry,
|
||||||
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
||||||
|
request_refresh_debouncer=Debouncer(
|
||||||
|
hass, _LOGGER, cooldown=30, immediate=False
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.api = AmazonEchoApi(
|
self.api = AmazonEchoApi(
|
||||||
session,
|
session,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import anthropic
|
import anthropic
|
||||||
@@ -283,7 +284,11 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_CHAT_MODEL,
|
CONF_CHAT_MODEL,
|
||||||
default=RECOMMENDED_CHAT_MODEL,
|
default=RECOMMENDED_CHAT_MODEL,
|
||||||
): str,
|
): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=await self._get_model_list(), custom_value=True
|
||||||
|
)
|
||||||
|
),
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_MAX_TOKENS,
|
CONF_MAX_TOKENS,
|
||||||
default=RECOMMENDED_MAX_TOKENS,
|
default=RECOMMENDED_MAX_TOKENS,
|
||||||
@@ -394,6 +399,39 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
last_step=True,
|
last_step=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _get_model_list(self) -> list[SelectOptionDict]:
|
||||||
|
"""Get list of available models."""
|
||||||
|
try:
|
||||||
|
client = await self.hass.async_add_executor_job(
|
||||||
|
partial(
|
||||||
|
anthropic.AsyncAnthropic,
|
||||||
|
api_key=self._get_entry().data[CONF_API_KEY],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
models = (await client.models.list()).data
|
||||||
|
except anthropic.AnthropicError:
|
||||||
|
models = []
|
||||||
|
_LOGGER.debug("Available models: %s", models)
|
||||||
|
model_options: list[SelectOptionDict] = []
|
||||||
|
short_form = re.compile(r"[^\d]-\d$")
|
||||||
|
for model_info in models:
|
||||||
|
# Resolve alias from versioned model name:
|
||||||
|
model_alias = (
|
||||||
|
model_info.id[:-9]
|
||||||
|
if model_info.id
|
||||||
|
not in ("claude-3-haiku-20240307", "claude-3-opus-20240229")
|
||||||
|
else model_info.id
|
||||||
|
)
|
||||||
|
if short_form.search(model_alias):
|
||||||
|
model_alias += "-0"
|
||||||
|
model_options.append(
|
||||||
|
SelectOptionDict(
|
||||||
|
label=model_info.display_name,
|
||||||
|
value=model_alias,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return model_options
|
||||||
|
|
||||||
async def _get_location_data(self) -> dict[str, str]:
|
async def _get_location_data(self) -> dict[str, str]:
|
||||||
"""Get approximate location data of the user."""
|
"""Get approximate location data of the user."""
|
||||||
location_data: dict[str, str] = {}
|
location_data: dict[str, str] = {}
|
||||||
|
|||||||
@@ -111,8 +111,6 @@ def handle_errors_and_zip[_AsusWrtBridgeT: AsusWrtBridge](
|
|||||||
|
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
return dict(zip(keys, list(data.values()), strict=False))
|
return dict(zip(keys, list(data.values()), strict=False))
|
||||||
if not isinstance(data, (list, tuple)):
|
|
||||||
raise UpdateFailed("Received invalid data type")
|
|
||||||
return dict(zip(keys, data, strict=False))
|
return dict(zip(keys, data, strict=False))
|
||||||
|
|
||||||
return _wrapper
|
return _wrapper
|
||||||
|
|||||||
@@ -74,8 +74,11 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
|||||||
super().__init__(data.fast_coordinator, data)
|
super().__init__(data.fast_coordinator, data)
|
||||||
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
|
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
|
||||||
|
|
||||||
self._attr_min_temp = data.static.min_temp.value
|
# Set temperature range if available, otherwise use Home Assistant defaults
|
||||||
self._attr_max_temp = data.static.max_temp.value
|
if data.static.min_temp is not None and data.static.min_temp.value is not None:
|
||||||
|
self._attr_min_temp = data.static.min_temp.value
|
||||||
|
if data.static.max_temp is not None and data.static.max_temp.value is not None:
|
||||||
|
self._attr_max_temp = data.static.max_temp.value
|
||||||
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit
|
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["bsblan"],
|
"loggers": ["bsblan"],
|
||||||
"requirements": ["python-bsblan==3.1.0"],
|
"requirements": ["python-bsblan==3.1.1"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"name": "bsb-lan*",
|
"name": "bsb-lan*",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ from .const import (
|
|||||||
CONF_ALIASES,
|
CONF_ALIASES,
|
||||||
CONF_API_SERVER,
|
CONF_API_SERVER,
|
||||||
CONF_COGNITO_CLIENT_ID,
|
CONF_COGNITO_CLIENT_ID,
|
||||||
|
CONF_DISCOVERY_SERVICE_ACTIONS,
|
||||||
CONF_ENTITY_CONFIG,
|
CONF_ENTITY_CONFIG,
|
||||||
CONF_FILTER,
|
CONF_FILTER,
|
||||||
CONF_GOOGLE_ACTIONS,
|
CONF_GOOGLE_ACTIONS,
|
||||||
@@ -139,6 +140,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
{
|
{
|
||||||
vol.Required(CONF_MODE): vol.In([MODE_DEV]),
|
vol.Required(CONF_MODE): vol.In([MODE_DEV]),
|
||||||
vol.Required(CONF_API_SERVER): str,
|
vol.Required(CONF_API_SERVER): str,
|
||||||
|
vol.Optional(CONF_DISCOVERY_SERVICE_ACTIONS): {str: cv.url},
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
_BASE_CONFIG_SCHEMA.extend(
|
_BASE_CONFIG_SCHEMA.extend(
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ CONF_ACCOUNT_LINK_SERVER = "account_link_server"
|
|||||||
CONF_ACCOUNTS_SERVER = "accounts_server"
|
CONF_ACCOUNTS_SERVER = "accounts_server"
|
||||||
CONF_ACME_SERVER = "acme_server"
|
CONF_ACME_SERVER = "acme_server"
|
||||||
CONF_API_SERVER = "api_server"
|
CONF_API_SERVER = "api_server"
|
||||||
|
CONF_DISCOVERY_SERVICE_ACTIONS = "discovery_service_actions"
|
||||||
CONF_RELAYER_SERVER = "relayer_server"
|
CONF_RELAYER_SERVER = "relayer_server"
|
||||||
CONF_REMOTESTATE_SERVER = "remotestate_server"
|
CONF_REMOTESTATE_SERVER = "remotestate_server"
|
||||||
CONF_SERVICEHANDLERS_SERVER = "servicehandlers_server"
|
CONF_SERVICEHANDLERS_SERVER = "servicehandlers_server"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["async_upnp_client"],
|
"loggers": ["async_upnp_client"],
|
||||||
"requirements": ["async-upnp-client==0.45.0", "getmac==0.9.5"],
|
"requirements": ["async-upnp-client==0.46.0", "getmac==0.9.5"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"dependencies": ["ssdp"],
|
"dependencies": ["ssdp"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["async-upnp-client==0.45.0"],
|
"requirements": ["async-upnp-client==0.46.0"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyecobee"],
|
"loggers": ["pyecobee"],
|
||||||
"requirements": ["python-ecobee-api==0.2.20"],
|
"requirements": ["python-ecobee-api==0.3.2"],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
|
|||||||
84
homeassistant/components/emoncms/quality_scale.yaml
Normal file
84
homeassistant/components/emoncms/quality_scale.yaml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
rules:
|
||||||
|
# todo : add get_feed_list to the library
|
||||||
|
# todo : see if we can drop some extra attributes
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration does not provide additional actions.
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage:
|
||||||
|
status: todo
|
||||||
|
comment: |
|
||||||
|
test_reconfigure_api_error should use a mock config entry fixture
|
||||||
|
test_user_flow_failure should use a mock config entry fixture
|
||||||
|
move test_user_flow_* to the top of the file
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration does not provide additional actions.
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No events are explicitly registered by the integration.
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure: done
|
||||||
|
test-before-setup: done
|
||||||
|
unique-config-entry: done
|
||||||
|
|
||||||
|
# Silver
|
||||||
|
action-exceptions: done
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters: done
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable: todo
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable: done
|
||||||
|
parallel-updates: todo
|
||||||
|
reauthentication-flow: todo
|
||||||
|
test-coverage:
|
||||||
|
status: todo
|
||||||
|
comment: |
|
||||||
|
test the entry state in test_failure
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: todo
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info: todo
|
||||||
|
discovery: todo
|
||||||
|
docs-data-update: done
|
||||||
|
docs-examples:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration does not provide any automation
|
||||||
|
docs-known-limitations: todo
|
||||||
|
docs-supported-devices: todo
|
||||||
|
docs-supported-functions: done
|
||||||
|
docs-troubleshooting: done
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices: todo
|
||||||
|
entity-category: todo
|
||||||
|
entity-device-class:
|
||||||
|
status: todo
|
||||||
|
comment: change device_class=SensorDeviceClass.SIGNAL_STRENGTH to SOUND_PRESSURE
|
||||||
|
entity-disabled-by-default: todo
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: todo
|
||||||
|
icon-translations: todo
|
||||||
|
reconfiguration-flow: done
|
||||||
|
repair-issues: todo
|
||||||
|
stale-devices: todo
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: todo
|
||||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from typing import Literal, TypedDict
|
from typing import Literal, NotRequired, TypedDict
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ async def async_get_manager(hass: HomeAssistant) -> EnergyManager:
|
|||||||
class FlowFromGridSourceType(TypedDict):
|
class FlowFromGridSourceType(TypedDict):
|
||||||
"""Dictionary describing the 'from' stat for the grid source."""
|
"""Dictionary describing the 'from' stat for the grid source."""
|
||||||
|
|
||||||
# statistic_id of a an energy meter (kWh)
|
# statistic_id of an energy meter (kWh)
|
||||||
stat_energy_from: str
|
stat_energy_from: str
|
||||||
|
|
||||||
# statistic_id of costs ($) incurred from the energy meter
|
# statistic_id of costs ($) incurred from the energy meter
|
||||||
@@ -58,6 +58,14 @@ class FlowToGridSourceType(TypedDict):
|
|||||||
number_energy_price: float | None # Price for energy ($/kWh)
|
number_energy_price: float | None # Price for energy ($/kWh)
|
||||||
|
|
||||||
|
|
||||||
|
class GridPowerSourceType(TypedDict):
|
||||||
|
"""Dictionary holding the source of grid power consumption."""
|
||||||
|
|
||||||
|
# statistic_id of a power meter (kW)
|
||||||
|
# negative values indicate grid return
|
||||||
|
stat_rate: str
|
||||||
|
|
||||||
|
|
||||||
class GridSourceType(TypedDict):
|
class GridSourceType(TypedDict):
|
||||||
"""Dictionary holding the source of grid energy consumption."""
|
"""Dictionary holding the source of grid energy consumption."""
|
||||||
|
|
||||||
@@ -65,6 +73,7 @@ class GridSourceType(TypedDict):
|
|||||||
|
|
||||||
flow_from: list[FlowFromGridSourceType]
|
flow_from: list[FlowFromGridSourceType]
|
||||||
flow_to: list[FlowToGridSourceType]
|
flow_to: list[FlowToGridSourceType]
|
||||||
|
power: NotRequired[list[GridPowerSourceType]]
|
||||||
|
|
||||||
cost_adjustment_day: float
|
cost_adjustment_day: float
|
||||||
|
|
||||||
@@ -75,6 +84,7 @@ class SolarSourceType(TypedDict):
|
|||||||
type: Literal["solar"]
|
type: Literal["solar"]
|
||||||
|
|
||||||
stat_energy_from: str
|
stat_energy_from: str
|
||||||
|
stat_rate: NotRequired[str]
|
||||||
config_entry_solar_forecast: list[str] | None
|
config_entry_solar_forecast: list[str] | None
|
||||||
|
|
||||||
|
|
||||||
@@ -85,6 +95,8 @@ class BatterySourceType(TypedDict):
|
|||||||
|
|
||||||
stat_energy_from: str
|
stat_energy_from: str
|
||||||
stat_energy_to: str
|
stat_energy_to: str
|
||||||
|
# positive when discharging, negative when charging
|
||||||
|
stat_rate: NotRequired[str]
|
||||||
|
|
||||||
|
|
||||||
class GasSourceType(TypedDict):
|
class GasSourceType(TypedDict):
|
||||||
@@ -136,12 +148,15 @@ class DeviceConsumption(TypedDict):
|
|||||||
# This is an ever increasing value
|
# This is an ever increasing value
|
||||||
stat_consumption: str
|
stat_consumption: str
|
||||||
|
|
||||||
|
# Instantaneous rate of flow: W, L/min or m³/h
|
||||||
|
stat_rate: NotRequired[str]
|
||||||
|
|
||||||
# An optional custom name for display in energy graphs
|
# An optional custom name for display in energy graphs
|
||||||
name: str | None
|
name: str | None
|
||||||
|
|
||||||
# An optional statistic_id identifying a device
|
# An optional statistic_id identifying a device
|
||||||
# that includes this device's consumption in its total
|
# that includes this device's consumption in its total
|
||||||
included_in_stat: str | None
|
included_in_stat: NotRequired[str]
|
||||||
|
|
||||||
|
|
||||||
class EnergyPreferences(TypedDict):
|
class EnergyPreferences(TypedDict):
|
||||||
@@ -194,6 +209,12 @@ FLOW_TO_GRID_SOURCE_SCHEMA = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
GRID_POWER_SOURCE_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required("stat_rate"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _generate_unique_value_validator(key: str) -> Callable[[list[dict]], list[dict]]:
|
def _generate_unique_value_validator(key: str) -> Callable[[list[dict]], list[dict]]:
|
||||||
"""Generate a validator that ensures a value is only used once."""
|
"""Generate a validator that ensures a value is only used once."""
|
||||||
@@ -224,6 +245,10 @@ GRID_SOURCE_SCHEMA = vol.Schema(
|
|||||||
[FLOW_TO_GRID_SOURCE_SCHEMA],
|
[FLOW_TO_GRID_SOURCE_SCHEMA],
|
||||||
_generate_unique_value_validator("stat_energy_to"),
|
_generate_unique_value_validator("stat_energy_to"),
|
||||||
),
|
),
|
||||||
|
vol.Optional("power"): vol.All(
|
||||||
|
[GRID_POWER_SOURCE_SCHEMA],
|
||||||
|
_generate_unique_value_validator("stat_rate"),
|
||||||
|
),
|
||||||
vol.Required("cost_adjustment_day"): vol.Coerce(float),
|
vol.Required("cost_adjustment_day"): vol.Coerce(float),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -231,6 +256,7 @@ SOLAR_SOURCE_SCHEMA = vol.Schema(
|
|||||||
{
|
{
|
||||||
vol.Required("type"): "solar",
|
vol.Required("type"): "solar",
|
||||||
vol.Required("stat_energy_from"): str,
|
vol.Required("stat_energy_from"): str,
|
||||||
|
vol.Optional("stat_rate"): str,
|
||||||
vol.Optional("config_entry_solar_forecast"): vol.Any([str], None),
|
vol.Optional("config_entry_solar_forecast"): vol.Any([str], None),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -239,6 +265,7 @@ BATTERY_SOURCE_SCHEMA = vol.Schema(
|
|||||||
vol.Required("type"): "battery",
|
vol.Required("type"): "battery",
|
||||||
vol.Required("stat_energy_from"): str,
|
vol.Required("stat_energy_from"): str,
|
||||||
vol.Required("stat_energy_to"): str,
|
vol.Required("stat_energy_to"): str,
|
||||||
|
vol.Optional("stat_rate"): str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
GAS_SOURCE_SCHEMA = vol.Schema(
|
GAS_SOURCE_SCHEMA = vol.Schema(
|
||||||
@@ -294,6 +321,7 @@ ENERGY_SOURCE_SCHEMA = vol.All(
|
|||||||
DEVICE_CONSUMPTION_SCHEMA = vol.Schema(
|
DEVICE_CONSUMPTION_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required("stat_consumption"): str,
|
vol.Required("stat_consumption"): str,
|
||||||
|
vol.Optional("stat_rate"): str,
|
||||||
vol.Optional("name"): str,
|
vol.Optional("name"): str,
|
||||||
vol.Optional("included_in_stat"): str,
|
vol.Optional("included_in_stat"): str,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from homeassistant.const import (
|
|||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
UnitOfEnergy,
|
UnitOfEnergy,
|
||||||
|
UnitOfPower,
|
||||||
UnitOfVolume,
|
UnitOfVolume,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback, valid_entity_id
|
from homeassistant.core import HomeAssistant, callback, valid_entity_id
|
||||||
@@ -23,12 +24,17 @@ ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,)
|
|||||||
ENERGY_USAGE_UNITS: dict[str, tuple[UnitOfEnergy, ...]] = {
|
ENERGY_USAGE_UNITS: dict[str, tuple[UnitOfEnergy, ...]] = {
|
||||||
sensor.SensorDeviceClass.ENERGY: tuple(UnitOfEnergy)
|
sensor.SensorDeviceClass.ENERGY: tuple(UnitOfEnergy)
|
||||||
}
|
}
|
||||||
|
POWER_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.POWER,)
|
||||||
|
POWER_USAGE_UNITS: dict[str, tuple[UnitOfPower, ...]] = {
|
||||||
|
sensor.SensorDeviceClass.POWER: tuple(UnitOfPower)
|
||||||
|
}
|
||||||
|
|
||||||
ENERGY_PRICE_UNITS = tuple(
|
ENERGY_PRICE_UNITS = tuple(
|
||||||
f"/{unit}" for units in ENERGY_USAGE_UNITS.values() for unit in units
|
f"/{unit}" for units in ENERGY_USAGE_UNITS.values() for unit in units
|
||||||
)
|
)
|
||||||
ENERGY_UNIT_ERROR = "entity_unexpected_unit_energy"
|
ENERGY_UNIT_ERROR = "entity_unexpected_unit_energy"
|
||||||
ENERGY_PRICE_UNIT_ERROR = "entity_unexpected_unit_energy_price"
|
ENERGY_PRICE_UNIT_ERROR = "entity_unexpected_unit_energy_price"
|
||||||
|
POWER_UNIT_ERROR = "entity_unexpected_unit_power"
|
||||||
GAS_USAGE_DEVICE_CLASSES = (
|
GAS_USAGE_DEVICE_CLASSES = (
|
||||||
sensor.SensorDeviceClass.ENERGY,
|
sensor.SensorDeviceClass.ENERGY,
|
||||||
sensor.SensorDeviceClass.GAS,
|
sensor.SensorDeviceClass.GAS,
|
||||||
@@ -82,6 +88,10 @@ def _get_placeholders(hass: HomeAssistant, issue_type: str) -> dict[str, str] |
|
|||||||
f"{currency}{unit}" for unit in ENERGY_PRICE_UNITS
|
f"{currency}{unit}" for unit in ENERGY_PRICE_UNITS
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
if issue_type == POWER_UNIT_ERROR:
|
||||||
|
return {
|
||||||
|
"power_units": ", ".join(POWER_USAGE_UNITS[sensor.SensorDeviceClass.POWER]),
|
||||||
|
}
|
||||||
if issue_type == GAS_UNIT_ERROR:
|
if issue_type == GAS_UNIT_ERROR:
|
||||||
return {
|
return {
|
||||||
"energy_units": ", ".join(GAS_USAGE_UNITS[sensor.SensorDeviceClass.ENERGY]),
|
"energy_units": ", ".join(GAS_USAGE_UNITS[sensor.SensorDeviceClass.ENERGY]),
|
||||||
@@ -159,7 +169,7 @@ class EnergyPreferencesValidation:
|
|||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_validate_usage_stat(
|
def _async_validate_stat_common(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]],
|
metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]],
|
||||||
stat_id: str,
|
stat_id: str,
|
||||||
@@ -167,37 +177,41 @@ def _async_validate_usage_stat(
|
|||||||
allowed_units: Mapping[str, Sequence[str]],
|
allowed_units: Mapping[str, Sequence[str]],
|
||||||
unit_error: str,
|
unit_error: str,
|
||||||
issues: ValidationIssues,
|
issues: ValidationIssues,
|
||||||
) -> None:
|
check_negative: bool = False,
|
||||||
"""Validate a statistic."""
|
) -> str | None:
|
||||||
|
"""Validate common aspects of a statistic.
|
||||||
|
|
||||||
|
Returns the entity_id if validation succeeds, None otherwise.
|
||||||
|
"""
|
||||||
if stat_id not in metadata:
|
if stat_id not in metadata:
|
||||||
issues.add_issue(hass, "statistics_not_defined", stat_id)
|
issues.add_issue(hass, "statistics_not_defined", stat_id)
|
||||||
|
|
||||||
has_entity_source = valid_entity_id(stat_id)
|
has_entity_source = valid_entity_id(stat_id)
|
||||||
|
|
||||||
if not has_entity_source:
|
if not has_entity_source:
|
||||||
return
|
return None
|
||||||
|
|
||||||
entity_id = stat_id
|
entity_id = stat_id
|
||||||
|
|
||||||
if not recorder.is_entity_recorded(hass, entity_id):
|
if not recorder.is_entity_recorded(hass, entity_id):
|
||||||
issues.add_issue(hass, "recorder_untracked", entity_id)
|
issues.add_issue(hass, "recorder_untracked", entity_id)
|
||||||
return
|
return None
|
||||||
|
|
||||||
if (state := hass.states.get(entity_id)) is None:
|
if (state := hass.states.get(entity_id)) is None:
|
||||||
issues.add_issue(hass, "entity_not_defined", entity_id)
|
issues.add_issue(hass, "entity_not_defined", entity_id)
|
||||||
return
|
return None
|
||||||
|
|
||||||
if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||||
issues.add_issue(hass, "entity_unavailable", entity_id, state.state)
|
issues.add_issue(hass, "entity_unavailable", entity_id, state.state)
|
||||||
return
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_value: float | None = float(state.state)
|
current_value: float | None = float(state.state)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
issues.add_issue(hass, "entity_state_non_numeric", entity_id, state.state)
|
issues.add_issue(hass, "entity_state_non_numeric", entity_id, state.state)
|
||||||
return
|
return None
|
||||||
|
|
||||||
if current_value is not None and current_value < 0:
|
if check_negative and current_value is not None and current_value < 0:
|
||||||
issues.add_issue(hass, "entity_negative_state", entity_id, current_value)
|
issues.add_issue(hass, "entity_negative_state", entity_id, current_value)
|
||||||
|
|
||||||
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||||
@@ -211,6 +225,36 @@ def _async_validate_usage_stat(
|
|||||||
if device_class and unit not in allowed_units.get(device_class, []):
|
if device_class and unit not in allowed_units.get(device_class, []):
|
||||||
issues.add_issue(hass, unit_error, entity_id, unit)
|
issues.add_issue(hass, unit_error, entity_id, unit)
|
||||||
|
|
||||||
|
return entity_id
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_validate_usage_stat(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]],
|
||||||
|
stat_id: str,
|
||||||
|
allowed_device_classes: Sequence[str],
|
||||||
|
allowed_units: Mapping[str, Sequence[str]],
|
||||||
|
unit_error: str,
|
||||||
|
issues: ValidationIssues,
|
||||||
|
) -> None:
|
||||||
|
"""Validate a statistic."""
|
||||||
|
entity_id = _async_validate_stat_common(
|
||||||
|
hass,
|
||||||
|
metadata,
|
||||||
|
stat_id,
|
||||||
|
allowed_device_classes,
|
||||||
|
allowed_units,
|
||||||
|
unit_error,
|
||||||
|
issues,
|
||||||
|
check_negative=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if entity_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is not None
|
||||||
state_class = state.attributes.get(sensor.ATTR_STATE_CLASS)
|
state_class = state.attributes.get(sensor.ATTR_STATE_CLASS)
|
||||||
|
|
||||||
allowed_state_classes = [
|
allowed_state_classes = [
|
||||||
@@ -255,6 +299,39 @@ def _async_validate_price_entity(
|
|||||||
issues.add_issue(hass, unit_error, entity_id, unit)
|
issues.add_issue(hass, unit_error, entity_id, unit)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_validate_power_stat(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]],
|
||||||
|
stat_id: str,
|
||||||
|
allowed_device_classes: Sequence[str],
|
||||||
|
allowed_units: Mapping[str, Sequence[str]],
|
||||||
|
unit_error: str,
|
||||||
|
issues: ValidationIssues,
|
||||||
|
) -> None:
|
||||||
|
"""Validate a power statistic."""
|
||||||
|
entity_id = _async_validate_stat_common(
|
||||||
|
hass,
|
||||||
|
metadata,
|
||||||
|
stat_id,
|
||||||
|
allowed_device_classes,
|
||||||
|
allowed_units,
|
||||||
|
unit_error,
|
||||||
|
issues,
|
||||||
|
check_negative=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if entity_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is not None
|
||||||
|
state_class = state.attributes.get(sensor.ATTR_STATE_CLASS)
|
||||||
|
|
||||||
|
if state_class != sensor.SensorStateClass.MEASUREMENT:
|
||||||
|
issues.add_issue(hass, "entity_unexpected_state_class", entity_id, state_class)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_validate_cost_stat(
|
def _async_validate_cost_stat(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -309,11 +386,260 @@ def _async_validate_auto_generated_cost_entity(
|
|||||||
issues.add_issue(hass, "recorder_untracked", cost_entity_id)
|
issues.add_issue(hass, "recorder_untracked", cost_entity_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_grid_source(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
source: data.GridSourceType,
|
||||||
|
statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]],
|
||||||
|
wanted_statistics_metadata: set[str],
|
||||||
|
source_result: ValidationIssues,
|
||||||
|
validate_calls: list[functools.partial[None]],
|
||||||
|
) -> None:
|
||||||
|
"""Validate grid energy source."""
|
||||||
|
flow_from: data.FlowFromGridSourceType
|
||||||
|
for flow_from in source["flow_from"]:
|
||||||
|
wanted_statistics_metadata.add(flow_from["stat_energy_from"])
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_usage_stat,
|
||||||
|
hass,
|
||||||
|
statistics_metadata,
|
||||||
|
flow_from["stat_energy_from"],
|
||||||
|
ENERGY_USAGE_DEVICE_CLASSES,
|
||||||
|
ENERGY_USAGE_UNITS,
|
||||||
|
ENERGY_UNIT_ERROR,
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (stat_cost := flow_from.get("stat_cost")) is not None:
|
||||||
|
wanted_statistics_metadata.add(stat_cost)
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_cost_stat,
|
||||||
|
hass,
|
||||||
|
statistics_metadata,
|
||||||
|
stat_cost,
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif (entity_energy_price := flow_from.get("entity_energy_price")) is not None:
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_price_entity,
|
||||||
|
hass,
|
||||||
|
entity_energy_price,
|
||||||
|
source_result,
|
||||||
|
ENERGY_PRICE_UNITS,
|
||||||
|
ENERGY_PRICE_UNIT_ERROR,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
flow_from.get("entity_energy_price") is not None
|
||||||
|
or flow_from.get("number_energy_price") is not None
|
||||||
|
):
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_auto_generated_cost_entity,
|
||||||
|
hass,
|
||||||
|
flow_from["stat_energy_from"],
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
flow_to: data.FlowToGridSourceType
|
||||||
|
for flow_to in source["flow_to"]:
|
||||||
|
wanted_statistics_metadata.add(flow_to["stat_energy_to"])
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_usage_stat,
|
||||||
|
hass,
|
||||||
|
statistics_metadata,
|
||||||
|
flow_to["stat_energy_to"],
|
||||||
|
ENERGY_USAGE_DEVICE_CLASSES,
|
||||||
|
ENERGY_USAGE_UNITS,
|
||||||
|
ENERGY_UNIT_ERROR,
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (stat_compensation := flow_to.get("stat_compensation")) is not None:
|
||||||
|
wanted_statistics_metadata.add(stat_compensation)
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_cost_stat,
|
||||||
|
hass,
|
||||||
|
statistics_metadata,
|
||||||
|
stat_compensation,
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif (entity_energy_price := flow_to.get("entity_energy_price")) is not None:
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_price_entity,
|
||||||
|
hass,
|
||||||
|
entity_energy_price,
|
||||||
|
source_result,
|
||||||
|
ENERGY_PRICE_UNITS,
|
||||||
|
ENERGY_PRICE_UNIT_ERROR,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
flow_to.get("entity_energy_price") is not None
|
||||||
|
or flow_to.get("number_energy_price") is not None
|
||||||
|
):
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_auto_generated_cost_entity,
|
||||||
|
hass,
|
||||||
|
flow_to["stat_energy_to"],
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for power_stat in source.get("power", []):
|
||||||
|
wanted_statistics_metadata.add(power_stat["stat_rate"])
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_power_stat,
|
||||||
|
hass,
|
||||||
|
statistics_metadata,
|
||||||
|
power_stat["stat_rate"],
|
||||||
|
POWER_USAGE_DEVICE_CLASSES,
|
||||||
|
POWER_USAGE_UNITS,
|
||||||
|
POWER_UNIT_ERROR,
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_gas_source(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
source: data.GasSourceType,
|
||||||
|
statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]],
|
||||||
|
wanted_statistics_metadata: set[str],
|
||||||
|
source_result: ValidationIssues,
|
||||||
|
validate_calls: list[functools.partial[None]],
|
||||||
|
) -> None:
|
||||||
|
"""Validate gas energy source."""
|
||||||
|
wanted_statistics_metadata.add(source["stat_energy_from"])
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_usage_stat,
|
||||||
|
hass,
|
||||||
|
statistics_metadata,
|
||||||
|
source["stat_energy_from"],
|
||||||
|
GAS_USAGE_DEVICE_CLASSES,
|
||||||
|
GAS_USAGE_UNITS,
|
||||||
|
GAS_UNIT_ERROR,
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (stat_cost := source.get("stat_cost")) is not None:
|
||||||
|
wanted_statistics_metadata.add(stat_cost)
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_cost_stat,
|
||||||
|
hass,
|
||||||
|
statistics_metadata,
|
||||||
|
stat_cost,
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif (entity_energy_price := source.get("entity_energy_price")) is not None:
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_price_entity,
|
||||||
|
hass,
|
||||||
|
entity_energy_price,
|
||||||
|
source_result,
|
||||||
|
GAS_PRICE_UNITS,
|
||||||
|
GAS_PRICE_UNIT_ERROR,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
source.get("entity_energy_price") is not None
|
||||||
|
or source.get("number_energy_price") is not None
|
||||||
|
):
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_auto_generated_cost_entity,
|
||||||
|
hass,
|
||||||
|
source["stat_energy_from"],
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_water_source(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
source: data.WaterSourceType,
|
||||||
|
statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]],
|
||||||
|
wanted_statistics_metadata: set[str],
|
||||||
|
source_result: ValidationIssues,
|
||||||
|
validate_calls: list[functools.partial[None]],
|
||||||
|
) -> None:
|
||||||
|
"""Validate water energy source."""
|
||||||
|
wanted_statistics_metadata.add(source["stat_energy_from"])
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_usage_stat,
|
||||||
|
hass,
|
||||||
|
statistics_metadata,
|
||||||
|
source["stat_energy_from"],
|
||||||
|
WATER_USAGE_DEVICE_CLASSES,
|
||||||
|
WATER_USAGE_UNITS,
|
||||||
|
WATER_UNIT_ERROR,
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (stat_cost := source.get("stat_cost")) is not None:
|
||||||
|
wanted_statistics_metadata.add(stat_cost)
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_cost_stat,
|
||||||
|
hass,
|
||||||
|
statistics_metadata,
|
||||||
|
stat_cost,
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif (entity_energy_price := source.get("entity_energy_price")) is not None:
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_price_entity,
|
||||||
|
hass,
|
||||||
|
entity_energy_price,
|
||||||
|
source_result,
|
||||||
|
WATER_PRICE_UNITS,
|
||||||
|
WATER_PRICE_UNIT_ERROR,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
source.get("entity_energy_price") is not None
|
||||||
|
or source.get("number_energy_price") is not None
|
||||||
|
):
|
||||||
|
validate_calls.append(
|
||||||
|
functools.partial(
|
||||||
|
_async_validate_auto_generated_cost_entity,
|
||||||
|
hass,
|
||||||
|
source["stat_energy_from"],
|
||||||
|
source_result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||||
"""Validate the energy configuration."""
|
"""Validate the energy configuration."""
|
||||||
manager: data.EnergyManager = await data.async_get_manager(hass)
|
manager: data.EnergyManager = await data.async_get_manager(hass)
|
||||||
statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]] = {}
|
statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]] = {}
|
||||||
validate_calls = []
|
validate_calls: list[functools.partial[None]] = []
|
||||||
wanted_statistics_metadata: set[str] = set()
|
wanted_statistics_metadata: set[str] = set()
|
||||||
|
|
||||||
result = EnergyPreferencesValidation()
|
result = EnergyPreferencesValidation()
|
||||||
@@ -327,215 +653,35 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
|||||||
result.energy_sources.append(source_result)
|
result.energy_sources.append(source_result)
|
||||||
|
|
||||||
if source["type"] == "grid":
|
if source["type"] == "grid":
|
||||||
flow: data.FlowFromGridSourceType | data.FlowToGridSourceType
|
_validate_grid_source(
|
||||||
for flow in source["flow_from"]:
|
hass,
|
||||||
wanted_statistics_metadata.add(flow["stat_energy_from"])
|
source,
|
||||||
validate_calls.append(
|
statistics_metadata,
|
||||||
functools.partial(
|
wanted_statistics_metadata,
|
||||||
_async_validate_usage_stat,
|
source_result,
|
||||||
hass,
|
validate_calls,
|
||||||
statistics_metadata,
|
)
|
||||||
flow["stat_energy_from"],
|
|
||||||
ENERGY_USAGE_DEVICE_CLASSES,
|
|
||||||
ENERGY_USAGE_UNITS,
|
|
||||||
ENERGY_UNIT_ERROR,
|
|
||||||
source_result,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (stat_cost := flow.get("stat_cost")) is not None:
|
|
||||||
wanted_statistics_metadata.add(stat_cost)
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_cost_stat,
|
|
||||||
hass,
|
|
||||||
statistics_metadata,
|
|
||||||
stat_cost,
|
|
||||||
source_result,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif (
|
|
||||||
entity_energy_price := flow.get("entity_energy_price")
|
|
||||||
) is not None:
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_price_entity,
|
|
||||||
hass,
|
|
||||||
entity_energy_price,
|
|
||||||
source_result,
|
|
||||||
ENERGY_PRICE_UNITS,
|
|
||||||
ENERGY_PRICE_UNIT_ERROR,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
flow.get("entity_energy_price") is not None
|
|
||||||
or flow.get("number_energy_price") is not None
|
|
||||||
):
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_auto_generated_cost_entity,
|
|
||||||
hass,
|
|
||||||
flow["stat_energy_from"],
|
|
||||||
source_result,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for flow in source["flow_to"]:
|
|
||||||
wanted_statistics_metadata.add(flow["stat_energy_to"])
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_usage_stat,
|
|
||||||
hass,
|
|
||||||
statistics_metadata,
|
|
||||||
flow["stat_energy_to"],
|
|
||||||
ENERGY_USAGE_DEVICE_CLASSES,
|
|
||||||
ENERGY_USAGE_UNITS,
|
|
||||||
ENERGY_UNIT_ERROR,
|
|
||||||
source_result,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (stat_compensation := flow.get("stat_compensation")) is not None:
|
|
||||||
wanted_statistics_metadata.add(stat_compensation)
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_cost_stat,
|
|
||||||
hass,
|
|
||||||
statistics_metadata,
|
|
||||||
stat_compensation,
|
|
||||||
source_result,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif (
|
|
||||||
entity_energy_price := flow.get("entity_energy_price")
|
|
||||||
) is not None:
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_price_entity,
|
|
||||||
hass,
|
|
||||||
entity_energy_price,
|
|
||||||
source_result,
|
|
||||||
ENERGY_PRICE_UNITS,
|
|
||||||
ENERGY_PRICE_UNIT_ERROR,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
flow.get("entity_energy_price") is not None
|
|
||||||
or flow.get("number_energy_price") is not None
|
|
||||||
):
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_auto_generated_cost_entity,
|
|
||||||
hass,
|
|
||||||
flow["stat_energy_to"],
|
|
||||||
source_result,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif source["type"] == "gas":
|
elif source["type"] == "gas":
|
||||||
wanted_statistics_metadata.add(source["stat_energy_from"])
|
_validate_gas_source(
|
||||||
validate_calls.append(
|
hass,
|
||||||
functools.partial(
|
source,
|
||||||
_async_validate_usage_stat,
|
statistics_metadata,
|
||||||
hass,
|
wanted_statistics_metadata,
|
||||||
statistics_metadata,
|
source_result,
|
||||||
source["stat_energy_from"],
|
validate_calls,
|
||||||
GAS_USAGE_DEVICE_CLASSES,
|
|
||||||
GAS_USAGE_UNITS,
|
|
||||||
GAS_UNIT_ERROR,
|
|
||||||
source_result,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (stat_cost := source.get("stat_cost")) is not None:
|
|
||||||
wanted_statistics_metadata.add(stat_cost)
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_cost_stat,
|
|
||||||
hass,
|
|
||||||
statistics_metadata,
|
|
||||||
stat_cost,
|
|
||||||
source_result,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif (entity_energy_price := source.get("entity_energy_price")) is not None:
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_price_entity,
|
|
||||||
hass,
|
|
||||||
entity_energy_price,
|
|
||||||
source_result,
|
|
||||||
GAS_PRICE_UNITS,
|
|
||||||
GAS_PRICE_UNIT_ERROR,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
source.get("entity_energy_price") is not None
|
|
||||||
or source.get("number_energy_price") is not None
|
|
||||||
):
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_auto_generated_cost_entity,
|
|
||||||
hass,
|
|
||||||
source["stat_energy_from"],
|
|
||||||
source_result,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif source["type"] == "water":
|
elif source["type"] == "water":
|
||||||
wanted_statistics_metadata.add(source["stat_energy_from"])
|
_validate_water_source(
|
||||||
validate_calls.append(
|
hass,
|
||||||
functools.partial(
|
source,
|
||||||
_async_validate_usage_stat,
|
statistics_metadata,
|
||||||
hass,
|
wanted_statistics_metadata,
|
||||||
statistics_metadata,
|
source_result,
|
||||||
source["stat_energy_from"],
|
validate_calls,
|
||||||
WATER_USAGE_DEVICE_CLASSES,
|
|
||||||
WATER_USAGE_UNITS,
|
|
||||||
WATER_UNIT_ERROR,
|
|
||||||
source_result,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (stat_cost := source.get("stat_cost")) is not None:
|
|
||||||
wanted_statistics_metadata.add(stat_cost)
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_cost_stat,
|
|
||||||
hass,
|
|
||||||
statistics_metadata,
|
|
||||||
stat_cost,
|
|
||||||
source_result,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif (entity_energy_price := source.get("entity_energy_price")) is not None:
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_price_entity,
|
|
||||||
hass,
|
|
||||||
entity_energy_price,
|
|
||||||
source_result,
|
|
||||||
WATER_PRICE_UNITS,
|
|
||||||
WATER_PRICE_UNIT_ERROR,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
source.get("entity_energy_price") is not None
|
|
||||||
or source.get("number_energy_price") is not None
|
|
||||||
):
|
|
||||||
validate_calls.append(
|
|
||||||
functools.partial(
|
|
||||||
_async_validate_auto_generated_cost_entity,
|
|
||||||
hass,
|
|
||||||
source["stat_energy_from"],
|
|
||||||
source_result,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif source["type"] == "solar":
|
elif source["type"] == "solar":
|
||||||
wanted_statistics_metadata.add(source["stat_energy_from"])
|
wanted_statistics_metadata.add(source["stat_energy_from"])
|
||||||
validate_calls.append(
|
validate_calls.append(
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ async def async_get_config_entry_diagnostics(
|
|||||||
"ctmeter_production_phases": envoy_data.ctmeter_production_phases,
|
"ctmeter_production_phases": envoy_data.ctmeter_production_phases,
|
||||||
"ctmeter_consumption_phases": envoy_data.ctmeter_consumption_phases,
|
"ctmeter_consumption_phases": envoy_data.ctmeter_consumption_phases,
|
||||||
"ctmeter_storage_phases": envoy_data.ctmeter_storage_phases,
|
"ctmeter_storage_phases": envoy_data.ctmeter_storage_phases,
|
||||||
|
"ctmeters": envoy_data.ctmeters,
|
||||||
|
"ctmeters_phases": envoy_data.ctmeters_phases,
|
||||||
"dry_contact_status": envoy_data.dry_contact_status,
|
"dry_contact_status": envoy_data.dry_contact_status,
|
||||||
"dry_contact_settings": envoy_data.dry_contact_settings,
|
"dry_contact_settings": envoy_data.dry_contact_settings,
|
||||||
"inverters": envoy_data.inverters,
|
"inverters": envoy_data.inverters,
|
||||||
@@ -179,6 +181,7 @@ async def async_get_config_entry_diagnostics(
|
|||||||
"ct_consumption_meter": envoy.consumption_meter_type,
|
"ct_consumption_meter": envoy.consumption_meter_type,
|
||||||
"ct_production_meter": envoy.production_meter_type,
|
"ct_production_meter": envoy.production_meter_type,
|
||||||
"ct_storage_meter": envoy.storage_meter_type,
|
"ct_storage_meter": envoy.storage_meter_type,
|
||||||
|
"ct_meters": list(envoy_data.ctmeters.keys()),
|
||||||
}
|
}
|
||||||
|
|
||||||
fixture_data: dict[str, Any] = {}
|
fixture_data: dict[str, Any] = {}
|
||||||
|
|||||||
@@ -399,330 +399,189 @@ class EnvoyCTSensorEntityDescription(SensorEntityDescription):
|
|||||||
cttype: str | None = None
|
cttype: str | None = None
|
||||||
|
|
||||||
|
|
||||||
CT_NET_CONSUMPTION_SENSORS = (
|
# All ct types unified in common setup
|
||||||
EnvoyCTSensorEntityDescription(
|
CT_SENSORS = (
|
||||||
key="lifetime_net_consumption",
|
[
|
||||||
translation_key="lifetime_net_consumption",
|
EnvoyCTSensorEntityDescription(
|
||||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
key=key,
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
translation_key=key,
|
||||||
device_class=SensorDeviceClass.ENERGY,
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||||
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
suggested_display_precision=3,
|
device_class=SensorDeviceClass.ENERGY,
|
||||||
value_fn=attrgetter("energy_delivered"),
|
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
||||||
on_phase=None,
|
suggested_display_precision=3,
|
||||||
cttype=CtType.NET_CONSUMPTION,
|
value_fn=attrgetter("energy_delivered"),
|
||||||
),
|
on_phase=None,
|
||||||
EnvoyCTSensorEntityDescription(
|
cttype=cttype,
|
||||||
key="lifetime_net_production",
|
)
|
||||||
translation_key="lifetime_net_production",
|
for cttype, key in (
|
||||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
(CtType.NET_CONSUMPTION, "lifetime_net_consumption"),
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
# Production CT energy_delivered is not used
|
||||||
device_class=SensorDeviceClass.ENERGY,
|
(CtType.STORAGE, "lifetime_battery_discharged"),
|
||||||
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
|
||||||
suggested_display_precision=3,
|
|
||||||
value_fn=attrgetter("energy_received"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.NET_CONSUMPTION,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="net_consumption",
|
|
||||||
translation_key="net_consumption",
|
|
||||||
native_unit_of_measurement=UnitOfPower.WATT,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
device_class=SensorDeviceClass.POWER,
|
|
||||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
|
||||||
suggested_display_precision=3,
|
|
||||||
value_fn=attrgetter("active_power"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.NET_CONSUMPTION,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="frequency",
|
|
||||||
translation_key="net_ct_frequency",
|
|
||||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
device_class=SensorDeviceClass.FREQUENCY,
|
|
||||||
suggested_display_precision=1,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
value_fn=attrgetter("frequency"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.NET_CONSUMPTION,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="voltage",
|
|
||||||
translation_key="net_ct_voltage",
|
|
||||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
device_class=SensorDeviceClass.VOLTAGE,
|
|
||||||
suggested_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
|
||||||
suggested_display_precision=1,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
value_fn=attrgetter("voltage"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.NET_CONSUMPTION,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="net_ct_current",
|
|
||||||
translation_key="net_ct_current",
|
|
||||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
device_class=SensorDeviceClass.CURRENT,
|
|
||||||
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
|
||||||
suggested_display_precision=3,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
value_fn=attrgetter("current"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.NET_CONSUMPTION,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="net_ct_powerfactor",
|
|
||||||
translation_key="net_ct_powerfactor",
|
|
||||||
device_class=SensorDeviceClass.POWER_FACTOR,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
suggested_display_precision=2,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
value_fn=attrgetter("power_factor"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.NET_CONSUMPTION,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="net_consumption_ct_metering_status",
|
|
||||||
translation_key="net_ct_metering_status",
|
|
||||||
device_class=SensorDeviceClass.ENUM,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
options=list(CtMeterStatus),
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
value_fn=attrgetter("metering_status"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.NET_CONSUMPTION,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="net_consumption_ct_status_flags",
|
|
||||||
translation_key="net_ct_status_flags",
|
|
||||||
state_class=None,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.NET_CONSUMPTION,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
CT_NET_CONSUMPTION_PHASE_SENSORS = {
|
|
||||||
(on_phase := PHASENAMES[phase]): [
|
|
||||||
replace(
|
|
||||||
sensor,
|
|
||||||
key=f"{sensor.key}_l{phase + 1}",
|
|
||||||
translation_key=f"{sensor.translation_key}_phase",
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
on_phase=on_phase,
|
|
||||||
translation_placeholders={"phase_name": f"l{phase + 1}"},
|
|
||||||
)
|
)
|
||||||
for sensor in list(CT_NET_CONSUMPTION_SENSORS)
|
|
||||||
]
|
]
|
||||||
for phase in range(3)
|
+ [
|
||||||
}
|
EnvoyCTSensorEntityDescription(
|
||||||
|
key=key,
|
||||||
CT_PRODUCTION_SENSORS = (
|
translation_key=key,
|
||||||
EnvoyCTSensorEntityDescription(
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||||
key="production_ct_frequency",
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
translation_key="production_ct_frequency",
|
device_class=SensorDeviceClass.ENERGY,
|
||||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
suggested_display_precision=3,
|
||||||
device_class=SensorDeviceClass.FREQUENCY,
|
value_fn=attrgetter("energy_received"),
|
||||||
suggested_display_precision=1,
|
on_phase=None,
|
||||||
entity_registry_enabled_default=False,
|
cttype=cttype,
|
||||||
value_fn=attrgetter("frequency"),
|
)
|
||||||
on_phase=None,
|
for cttype, key in (
|
||||||
cttype=CtType.PRODUCTION,
|
(CtType.NET_CONSUMPTION, "lifetime_net_production"),
|
||||||
),
|
# Production CT energy_received is not used
|
||||||
EnvoyCTSensorEntityDescription(
|
(CtType.STORAGE, "lifetime_battery_charged"),
|
||||||
key="production_ct_voltage",
|
)
|
||||||
translation_key="production_ct_voltage",
|
]
|
||||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
+ [
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
EnvoyCTSensorEntityDescription(
|
||||||
device_class=SensorDeviceClass.VOLTAGE,
|
key=key,
|
||||||
suggested_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
translation_key=key,
|
||||||
suggested_display_precision=1,
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
entity_registry_enabled_default=False,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=attrgetter("voltage"),
|
device_class=SensorDeviceClass.POWER,
|
||||||
on_phase=None,
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
cttype=CtType.PRODUCTION,
|
suggested_display_precision=3,
|
||||||
),
|
value_fn=attrgetter("active_power"),
|
||||||
EnvoyCTSensorEntityDescription(
|
on_phase=None,
|
||||||
key="production_ct_current",
|
cttype=cttype,
|
||||||
translation_key="production_ct_current",
|
)
|
||||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
for cttype, key in (
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
(CtType.NET_CONSUMPTION, "net_consumption"),
|
||||||
device_class=SensorDeviceClass.CURRENT,
|
# Production CT active_power is not used
|
||||||
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
(CtType.STORAGE, "battery_discharge"),
|
||||||
suggested_display_precision=3,
|
)
|
||||||
entity_registry_enabled_default=False,
|
]
|
||||||
value_fn=attrgetter("current"),
|
+ [
|
||||||
on_phase=None,
|
EnvoyCTSensorEntityDescription(
|
||||||
cttype=CtType.PRODUCTION,
|
key=key,
|
||||||
),
|
translation_key=(translation_key if translation_key != "" else key),
|
||||||
EnvoyCTSensorEntityDescription(
|
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||||
key="production_ct_powerfactor",
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
translation_key="production_ct_powerfactor",
|
device_class=SensorDeviceClass.FREQUENCY,
|
||||||
device_class=SensorDeviceClass.POWER_FACTOR,
|
suggested_display_precision=1,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
entity_registry_enabled_default=False,
|
||||||
suggested_display_precision=2,
|
value_fn=attrgetter("frequency"),
|
||||||
entity_registry_enabled_default=False,
|
on_phase=None,
|
||||||
value_fn=attrgetter("power_factor"),
|
cttype=cttype,
|
||||||
on_phase=None,
|
)
|
||||||
cttype=CtType.PRODUCTION,
|
for cttype, key, translation_key in (
|
||||||
),
|
(CtType.NET_CONSUMPTION, "frequency", "net_ct_frequency"),
|
||||||
EnvoyCTSensorEntityDescription(
|
(CtType.PRODUCTION, "production_ct_frequency", ""),
|
||||||
key="production_ct_metering_status",
|
(CtType.STORAGE, "storage_ct_frequency", ""),
|
||||||
translation_key="production_ct_metering_status",
|
)
|
||||||
device_class=SensorDeviceClass.ENUM,
|
]
|
||||||
options=list(CtMeterStatus),
|
+ [
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
EnvoyCTSensorEntityDescription(
|
||||||
entity_registry_enabled_default=False,
|
key=key,
|
||||||
value_fn=attrgetter("metering_status"),
|
translation_key=(translation_key if translation_key != "" else key),
|
||||||
on_phase=None,
|
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||||
cttype=CtType.PRODUCTION,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
device_class=SensorDeviceClass.VOLTAGE,
|
||||||
EnvoyCTSensorEntityDescription(
|
suggested_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||||
key="production_ct_status_flags",
|
suggested_display_precision=1,
|
||||||
translation_key="production_ct_status_flags",
|
entity_registry_enabled_default=False,
|
||||||
state_class=None,
|
value_fn=attrgetter("voltage"),
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
on_phase=None,
|
||||||
entity_registry_enabled_default=False,
|
cttype=cttype,
|
||||||
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
|
)
|
||||||
on_phase=None,
|
for cttype, key, translation_key in (
|
||||||
cttype=CtType.PRODUCTION,
|
(CtType.NET_CONSUMPTION, "voltage", "net_ct_voltage"),
|
||||||
),
|
(CtType.PRODUCTION, "production_ct_voltage", ""),
|
||||||
)
|
(CtType.STORAGE, "storage_voltage", "storage_ct_voltage"),
|
||||||
|
)
|
||||||
CT_PRODUCTION_PHASE_SENSORS = {
|
]
|
||||||
(on_phase := PHASENAMES[phase]): [
|
+ [
|
||||||
replace(
|
EnvoyCTSensorEntityDescription(
|
||||||
sensor,
|
key=key,
|
||||||
key=f"{sensor.key}_l{phase + 1}",
|
translation_key=key,
|
||||||
translation_key=f"{sensor.translation_key}_phase",
|
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||||
entity_registry_enabled_default=False,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
on_phase=on_phase,
|
device_class=SensorDeviceClass.CURRENT,
|
||||||
translation_placeholders={"phase_name": f"l{phase + 1}"},
|
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||||
|
suggested_display_precision=3,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
value_fn=attrgetter("current"),
|
||||||
|
on_phase=None,
|
||||||
|
cttype=cttype,
|
||||||
|
)
|
||||||
|
for cttype, key in (
|
||||||
|
(CtType.NET_CONSUMPTION, "net_ct_current"),
|
||||||
|
(CtType.PRODUCTION, "production_ct_current"),
|
||||||
|
(CtType.STORAGE, "storage_ct_current"),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
EnvoyCTSensorEntityDescription(
|
||||||
|
key=key,
|
||||||
|
translation_key=key,
|
||||||
|
device_class=SensorDeviceClass.POWER_FACTOR,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
value_fn=attrgetter("power_factor"),
|
||||||
|
on_phase=None,
|
||||||
|
cttype=cttype,
|
||||||
|
)
|
||||||
|
for cttype, key in (
|
||||||
|
(CtType.NET_CONSUMPTION, "net_ct_powerfactor"),
|
||||||
|
(CtType.PRODUCTION, "production_ct_powerfactor"),
|
||||||
|
(CtType.STORAGE, "storage_ct_powerfactor"),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
EnvoyCTSensorEntityDescription(
|
||||||
|
key=key,
|
||||||
|
translation_key=(translation_key if translation_key != "" else key),
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
options=list(CtMeterStatus),
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
value_fn=attrgetter("metering_status"),
|
||||||
|
on_phase=None,
|
||||||
|
cttype=cttype,
|
||||||
|
)
|
||||||
|
for cttype, key, translation_key in (
|
||||||
|
(
|
||||||
|
CtType.NET_CONSUMPTION,
|
||||||
|
"net_consumption_ct_metering_status",
|
||||||
|
"net_ct_metering_status",
|
||||||
|
),
|
||||||
|
(CtType.PRODUCTION, "production_ct_metering_status", ""),
|
||||||
|
(CtType.STORAGE, "storage_ct_metering_status", ""),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
EnvoyCTSensorEntityDescription(
|
||||||
|
key=key,
|
||||||
|
translation_key=(translation_key if translation_key != "" else key),
|
||||||
|
state_class=None,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
|
||||||
|
on_phase=None,
|
||||||
|
cttype=cttype,
|
||||||
|
)
|
||||||
|
for cttype, key, translation_key in (
|
||||||
|
(
|
||||||
|
CtType.NET_CONSUMPTION,
|
||||||
|
"net_consumption_ct_status_flags",
|
||||||
|
"net_ct_status_flags",
|
||||||
|
),
|
||||||
|
(CtType.PRODUCTION, "production_ct_status_flags", ""),
|
||||||
|
(CtType.STORAGE, "storage_ct_status_flags", ""),
|
||||||
)
|
)
|
||||||
for sensor in list(CT_PRODUCTION_SENSORS)
|
|
||||||
]
|
]
|
||||||
for phase in range(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
CT_STORAGE_SENSORS = (
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="lifetime_battery_discharged",
|
|
||||||
translation_key="lifetime_battery_discharged",
|
|
||||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
|
||||||
device_class=SensorDeviceClass.ENERGY,
|
|
||||||
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
|
||||||
suggested_display_precision=3,
|
|
||||||
value_fn=attrgetter("energy_delivered"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.STORAGE,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="lifetime_battery_charged",
|
|
||||||
translation_key="lifetime_battery_charged",
|
|
||||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
|
||||||
device_class=SensorDeviceClass.ENERGY,
|
|
||||||
suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR,
|
|
||||||
suggested_display_precision=3,
|
|
||||||
value_fn=attrgetter("energy_received"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.STORAGE,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="battery_discharge",
|
|
||||||
translation_key="battery_discharge",
|
|
||||||
native_unit_of_measurement=UnitOfPower.WATT,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
device_class=SensorDeviceClass.POWER,
|
|
||||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
|
||||||
suggested_display_precision=3,
|
|
||||||
value_fn=attrgetter("active_power"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.STORAGE,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="storage_ct_frequency",
|
|
||||||
translation_key="storage_ct_frequency",
|
|
||||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
device_class=SensorDeviceClass.FREQUENCY,
|
|
||||||
suggested_display_precision=1,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
value_fn=attrgetter("frequency"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.STORAGE,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="storage_voltage",
|
|
||||||
translation_key="storage_ct_voltage",
|
|
||||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
device_class=SensorDeviceClass.VOLTAGE,
|
|
||||||
suggested_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
|
||||||
suggested_display_precision=1,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
value_fn=attrgetter("voltage"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.STORAGE,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="storage_ct_current",
|
|
||||||
translation_key="storage_ct_current",
|
|
||||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
device_class=SensorDeviceClass.CURRENT,
|
|
||||||
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
|
||||||
suggested_display_precision=3,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
value_fn=attrgetter("current"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.STORAGE,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="storage_ct_powerfactor",
|
|
||||||
translation_key="storage_ct_powerfactor",
|
|
||||||
device_class=SensorDeviceClass.POWER_FACTOR,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
suggested_display_precision=2,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
value_fn=attrgetter("power_factor"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.STORAGE,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="storage_ct_metering_status",
|
|
||||||
translation_key="storage_ct_metering_status",
|
|
||||||
device_class=SensorDeviceClass.ENUM,
|
|
||||||
options=list(CtMeterStatus),
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
value_fn=attrgetter("metering_status"),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.STORAGE,
|
|
||||||
),
|
|
||||||
EnvoyCTSensorEntityDescription(
|
|
||||||
key="storage_ct_status_flags",
|
|
||||||
translation_key="storage_ct_status_flags",
|
|
||||||
state_class=None,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags),
|
|
||||||
on_phase=None,
|
|
||||||
cttype=CtType.STORAGE,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
CT_STORAGE_PHASE_SENSORS = {
|
CT_PHASE_SENSORS = {
|
||||||
(on_phase := PHASENAMES[phase]): [
|
(on_phase := PHASENAMES[phase]): [
|
||||||
replace(
|
replace(
|
||||||
sensor,
|
sensor,
|
||||||
@@ -732,7 +591,7 @@ CT_STORAGE_PHASE_SENSORS = {
|
|||||||
on_phase=on_phase,
|
on_phase=on_phase,
|
||||||
translation_placeholders={"phase_name": f"l{phase + 1}"},
|
translation_placeholders={"phase_name": f"l{phase + 1}"},
|
||||||
)
|
)
|
||||||
for sensor in list(CT_STORAGE_SENSORS)
|
for sensor in list(CT_SENSORS)
|
||||||
]
|
]
|
||||||
for phase in range(3)
|
for phase in range(3)
|
||||||
}
|
}
|
||||||
@@ -1060,24 +919,14 @@ async def async_setup_entry(
|
|||||||
if envoy_data.ctmeters:
|
if envoy_data.ctmeters:
|
||||||
entities.extend(
|
entities.extend(
|
||||||
EnvoyCTEntity(coordinator, description)
|
EnvoyCTEntity(coordinator, description)
|
||||||
for sensors in (
|
for description in CT_SENSORS
|
||||||
CT_NET_CONSUMPTION_SENSORS,
|
|
||||||
CT_PRODUCTION_SENSORS,
|
|
||||||
CT_STORAGE_SENSORS,
|
|
||||||
)
|
|
||||||
for description in sensors
|
|
||||||
if description.cttype in envoy_data.ctmeters
|
if description.cttype in envoy_data.ctmeters
|
||||||
)
|
)
|
||||||
# Add Current Transformer phase entities
|
# Add Current Transformer phase entities
|
||||||
if ctmeters_phases := envoy_data.ctmeters_phases:
|
if ctmeters_phases := envoy_data.ctmeters_phases:
|
||||||
entities.extend(
|
entities.extend(
|
||||||
EnvoyCTPhaseEntity(coordinator, description)
|
EnvoyCTPhaseEntity(coordinator, description)
|
||||||
for sensors in (
|
for phase, descriptions in CT_PHASE_SENSORS.items()
|
||||||
CT_NET_CONSUMPTION_PHASE_SENSORS,
|
|
||||||
CT_PRODUCTION_PHASE_SENSORS,
|
|
||||||
CT_STORAGE_PHASE_SENSORS,
|
|
||||||
)
|
|
||||||
for phase, descriptions in sensors.items()
|
|
||||||
for description in descriptions
|
for description in descriptions
|
||||||
if (cttype := description.cttype) in ctmeters_phases
|
if (cttype := description.cttype) in ctmeters_phases
|
||||||
and phase in ctmeters_phases[cttype]
|
and phase in ctmeters_phases[cttype]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -152,7 +153,9 @@ class HassFoscamCamera(FoscamEntity, Camera):
|
|||||||
async def stream_source(self) -> str | None:
|
async def stream_source(self) -> str | None:
|
||||||
"""Return the stream source."""
|
"""Return the stream source."""
|
||||||
if self._rtsp_port:
|
if self._rtsp_port:
|
||||||
return f"rtsp://{self._username}:{self._password}@{self._foscam_session.host}:{self._rtsp_port}/video{self._stream}"
|
_username = quote(self._username)
|
||||||
|
_password = quote(self._password)
|
||||||
|
return f"rtsp://{_username}:{_password}@{self._foscam_session.host}:{self._rtsp_port}/video{self._stream}"
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -481,6 +481,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
sidebar_title="climate",
|
sidebar_title="climate",
|
||||||
sidebar_default_visible=False,
|
sidebar_default_visible=False,
|
||||||
)
|
)
|
||||||
|
async_register_built_in_panel(
|
||||||
|
hass,
|
||||||
|
"home",
|
||||||
|
sidebar_icon="mdi:home",
|
||||||
|
sidebar_title="home",
|
||||||
|
sidebar_default_visible=False,
|
||||||
|
)
|
||||||
|
|
||||||
async_register_built_in_panel(hass, "profile")
|
async_register_built_in_panel(hass, "profile")
|
||||||
|
|
||||||
@@ -770,7 +777,9 @@ class ManifestJSONView(HomeAssistantView):
|
|||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
{
|
{
|
||||||
"type": "frontend/get_icons",
|
"type": "frontend/get_icons",
|
||||||
vol.Required("category"): vol.In({"entity", "entity_component", "services"}),
|
vol.Required("category"): vol.In(
|
||||||
|
{"entity", "entity_component", "services", "triggers", "conditions"}
|
||||||
|
),
|
||||||
vol.Optional("integration"): vol.All(cv.ensure_list, [str]),
|
vol.Optional("integration"): vol.All(cv.ensure_list, [str]),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
"""The Goodwe inverter component."""
|
"""The Goodwe inverter component."""
|
||||||
|
|
||||||
from goodwe import InverterError, connect
|
from goodwe import Inverter, InverterError, connect
|
||||||
from goodwe.const import GOODWE_TCP_PORT, GOODWE_UDP_PORT
|
from goodwe.const import GOODWE_UDP_PORT
|
||||||
|
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
|
||||||
|
from .config_flow import GoodweFlowHandler
|
||||||
from .const import CONF_MODEL_FAMILY, DOMAIN, PLATFORMS
|
from .const import CONF_MODEL_FAMILY, DOMAIN, PLATFORMS
|
||||||
from .coordinator import GoodweConfigEntry, GoodweRuntimeData, GoodweUpdateCoordinator
|
from .coordinator import GoodweConfigEntry, GoodweRuntimeData, GoodweUpdateCoordinator
|
||||||
|
|
||||||
@@ -15,28 +16,22 @@ from .coordinator import GoodweConfigEntry, GoodweRuntimeData, GoodweUpdateCoord
|
|||||||
async def async_setup_entry(hass: HomeAssistant, entry: GoodweConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: GoodweConfigEntry) -> bool:
|
||||||
"""Set up the Goodwe components from a config entry."""
|
"""Set up the Goodwe components from a config entry."""
|
||||||
host = entry.data[CONF_HOST]
|
host = entry.data[CONF_HOST]
|
||||||
|
port = entry.data.get(CONF_PORT, GOODWE_UDP_PORT)
|
||||||
model_family = entry.data[CONF_MODEL_FAMILY]
|
model_family = entry.data[CONF_MODEL_FAMILY]
|
||||||
|
|
||||||
# Connect to Goodwe inverter
|
# Connect to Goodwe inverter
|
||||||
try:
|
try:
|
||||||
inverter = await connect(
|
inverter = await connect(
|
||||||
host=host,
|
host=host,
|
||||||
port=GOODWE_UDP_PORT,
|
port=port,
|
||||||
family=model_family,
|
family=model_family,
|
||||||
retries=10,
|
retries=10,
|
||||||
)
|
)
|
||||||
except InverterError as err_udp:
|
except InverterError as err:
|
||||||
# First try with UDP failed, trying with the TCP port
|
|
||||||
try:
|
try:
|
||||||
inverter = await connect(
|
inverter = await async_check_port(hass, entry, host)
|
||||||
host=host,
|
|
||||||
port=GOODWE_TCP_PORT,
|
|
||||||
family=model_family,
|
|
||||||
retries=10,
|
|
||||||
)
|
|
||||||
except InverterError:
|
except InverterError:
|
||||||
# Both ports are unavailable
|
raise ConfigEntryNotReady from err
|
||||||
raise ConfigEntryNotReady from err_udp
|
|
||||||
|
|
||||||
device_info = DeviceInfo(
|
device_info = DeviceInfo(
|
||||||
configuration_url="https://www.semsportal.com",
|
configuration_url="https://www.semsportal.com",
|
||||||
@@ -66,6 +61,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoodweConfigEntry) -> bo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_check_port(
|
||||||
|
hass: HomeAssistant, entry: GoodweConfigEntry, host: str
|
||||||
|
) -> Inverter:
|
||||||
|
"""Check the communication port of the inverter, it may have changed after a firmware update."""
|
||||||
|
inverter, port = await GoodweFlowHandler.async_detect_inverter_port(host=host)
|
||||||
|
family = type(inverter).__name__
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry,
|
||||||
|
data={
|
||||||
|
CONF_HOST: host,
|
||||||
|
CONF_PORT: port,
|
||||||
|
CONF_MODEL_FAMILY: family,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return inverter
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(
|
async def async_unload_entry(
|
||||||
hass: HomeAssistant, config_entry: GoodweConfigEntry
|
hass: HomeAssistant, config_entry: GoodweConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -76,3 +88,31 @@ async def async_unload_entry(
|
|||||||
async def update_listener(hass: HomeAssistant, config_entry: GoodweConfigEntry) -> None:
|
async def update_listener(hass: HomeAssistant, config_entry: GoodweConfigEntry) -> None:
|
||||||
"""Handle options update."""
|
"""Handle options update."""
|
||||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(
|
||||||
|
hass: HomeAssistant, config_entry: GoodweConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Migrate old config entries."""
|
||||||
|
|
||||||
|
if config_entry.version > 2:
|
||||||
|
# This means the user has downgraded from a future version
|
||||||
|
return False
|
||||||
|
|
||||||
|
if config_entry.version == 1:
|
||||||
|
# Update from version 1 to version 2 adding the PROTOCOL to the config entry
|
||||||
|
host = config_entry.data[CONF_HOST]
|
||||||
|
try:
|
||||||
|
inverter, port = await GoodweFlowHandler.async_detect_inverter_port(
|
||||||
|
host=host
|
||||||
|
)
|
||||||
|
except InverterError as err:
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
|
new_data = {
|
||||||
|
CONF_HOST: host,
|
||||||
|
CONF_PORT: port,
|
||||||
|
CONF_MODEL_FAMILY: type(inverter).__name__,
|
||||||
|
}
|
||||||
|
hass.config_entries.async_update_entry(config_entry, data=new_data, version=2)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from goodwe import InverterError, connect
|
from goodwe import Inverter, InverterError, connect
|
||||||
from goodwe.const import GOODWE_TCP_PORT, GOODWE_UDP_PORT
|
from goodwe.const import GOODWE_TCP_PORT, GOODWE_UDP_PORT
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
|
||||||
from .const import CONF_MODEL_FAMILY, DEFAULT_NAME, DOMAIN
|
from .const import CONF_MODEL_FAMILY, DEFAULT_NAME, DOMAIN
|
||||||
|
|
||||||
@@ -26,9 +26,15 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class GoodweFlowHandler(ConfigFlow, domain=DOMAIN):
|
class GoodweFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a Goodwe config flow."""
|
"""Handle a Goodwe config flow."""
|
||||||
|
|
||||||
VERSION = 1
|
MINOR_VERSION = 2
|
||||||
|
|
||||||
async def _handle_successful_connection(self, inverter, host):
|
async def async_handle_successful_connection(
|
||||||
|
self,
|
||||||
|
inverter: Inverter,
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a successful connection storing it's values on the entry data."""
|
||||||
await self.async_set_unique_id(inverter.serial_number)
|
await self.async_set_unique_id(inverter.serial_number)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
@@ -36,6 +42,7 @@ class GoodweFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
title=DEFAULT_NAME,
|
title=DEFAULT_NAME,
|
||||||
data={
|
data={
|
||||||
CONF_HOST: host,
|
CONF_HOST: host,
|
||||||
|
CONF_PORT: port,
|
||||||
CONF_MODEL_FAMILY: type(inverter).__name__,
|
CONF_MODEL_FAMILY: type(inverter).__name__,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -48,19 +55,26 @@ class GoodweFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
host = user_input[CONF_HOST]
|
host = user_input[CONF_HOST]
|
||||||
try:
|
try:
|
||||||
inverter = await connect(host=host, port=GOODWE_UDP_PORT, retries=10)
|
inverter, port = await self.async_detect_inverter_port(host=host)
|
||||||
except InverterError:
|
except InverterError:
|
||||||
try:
|
errors[CONF_HOST] = "connection_error"
|
||||||
inverter = await connect(
|
|
||||||
host=host, port=GOODWE_TCP_PORT, retries=10
|
|
||||||
)
|
|
||||||
except InverterError:
|
|
||||||
errors[CONF_HOST] = "connection_error"
|
|
||||||
else:
|
|
||||||
return await self._handle_successful_connection(inverter, host)
|
|
||||||
else:
|
else:
|
||||||
return await self._handle_successful_connection(inverter, host)
|
return await self.async_handle_successful_connection(
|
||||||
|
inverter, host, port
|
||||||
|
)
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
|
step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def async_detect_inverter_port(
|
||||||
|
host: str,
|
||||||
|
) -> tuple[Inverter, int]:
|
||||||
|
"""Detects the port of the Inverter."""
|
||||||
|
port = GOODWE_UDP_PORT
|
||||||
|
try:
|
||||||
|
inverter = await connect(host=host, port=port, retries=10)
|
||||||
|
except InverterError:
|
||||||
|
port = GOODWE_TCP_PORT
|
||||||
|
inverter = await connect(host=host, port=port, retries=10)
|
||||||
|
return inverter, port
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["googleapiclient"],
|
"loggers": ["googleapiclient"],
|
||||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==11.0.0"]
|
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==11.1.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ def _convert_api_item(item: dict[str, str]) -> TodoItem:
|
|||||||
# Due dates are returned always in UTC so we only need to
|
# Due dates are returned always in UTC so we only need to
|
||||||
# parse the date portion which will be interpreted as a a local date.
|
# parse the date portion which will be interpreted as a a local date.
|
||||||
due = datetime.fromisoformat(due_str).date()
|
due = datetime.fromisoformat(due_str).date()
|
||||||
|
completed: datetime | None = None
|
||||||
|
if (completed_str := item.get("completed")) is not None:
|
||||||
|
completed = datetime.fromisoformat(completed_str)
|
||||||
return TodoItem(
|
return TodoItem(
|
||||||
summary=item["title"],
|
summary=item["title"],
|
||||||
uid=item["id"],
|
uid=item["id"],
|
||||||
@@ -61,6 +64,7 @@ def _convert_api_item(item: dict[str, str]) -> TodoItem:
|
|||||||
TodoItemStatus.NEEDS_ACTION,
|
TodoItemStatus.NEEDS_ACTION,
|
||||||
),
|
),
|
||||||
due=due,
|
due=due,
|
||||||
|
completed=completed,
|
||||||
description=item.get("notes"),
|
description=item.get("notes"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
84
homeassistant/components/google_weather/__init__.py
Normal file
84
homeassistant/components/google_weather/__init__.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
"""The Google Weather integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from google_weather_api import GoogleWeatherApi
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_API_KEY, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import CONF_REFERRER
|
||||||
|
from .coordinator import (
|
||||||
|
GoogleWeatherConfigEntry,
|
||||||
|
GoogleWeatherCurrentConditionsCoordinator,
|
||||||
|
GoogleWeatherDailyForecastCoordinator,
|
||||||
|
GoogleWeatherHourlyForecastCoordinator,
|
||||||
|
GoogleWeatherRuntimeData,
|
||||||
|
GoogleWeatherSubEntryRuntimeData,
|
||||||
|
)
|
||||||
|
|
||||||
|
_PLATFORMS: list[Platform] = [Platform.WEATHER]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: GoogleWeatherConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Set up Google Weather from a config entry."""
|
||||||
|
|
||||||
|
api = GoogleWeatherApi(
|
||||||
|
session=async_get_clientsession(hass),
|
||||||
|
api_key=entry.data[CONF_API_KEY],
|
||||||
|
referrer=entry.data.get(CONF_REFERRER),
|
||||||
|
language_code=hass.config.language,
|
||||||
|
)
|
||||||
|
subentries_runtime_data: dict[str, GoogleWeatherSubEntryRuntimeData] = {}
|
||||||
|
for subentry in entry.subentries.values():
|
||||||
|
subentry_runtime_data = GoogleWeatherSubEntryRuntimeData(
|
||||||
|
coordinator_observation=GoogleWeatherCurrentConditionsCoordinator(
|
||||||
|
hass, entry, subentry, api
|
||||||
|
),
|
||||||
|
coordinator_daily_forecast=GoogleWeatherDailyForecastCoordinator(
|
||||||
|
hass, entry, subentry, api
|
||||||
|
),
|
||||||
|
coordinator_hourly_forecast=GoogleWeatherHourlyForecastCoordinator(
|
||||||
|
hass, entry, subentry, api
|
||||||
|
),
|
||||||
|
)
|
||||||
|
subentries_runtime_data[subentry.subentry_id] = subentry_runtime_data
|
||||||
|
tasks = [
|
||||||
|
coro
|
||||||
|
for subentry_runtime_data in subentries_runtime_data.values()
|
||||||
|
for coro in (
|
||||||
|
subentry_runtime_data.coordinator_observation.async_config_entry_first_refresh(),
|
||||||
|
subentry_runtime_data.coordinator_daily_forecast.async_config_entry_first_refresh(),
|
||||||
|
subentry_runtime_data.coordinator_hourly_forecast.async_config_entry_first_refresh(),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
entry.runtime_data = GoogleWeatherRuntimeData(
|
||||||
|
api=api,
|
||||||
|
subentries_runtime_data=subentries_runtime_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||||
|
|
||||||
|
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: GoogleWeatherConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_update_options(
|
||||||
|
hass: HomeAssistant, entry: GoogleWeatherConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Update options."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
198
homeassistant/components/google_weather/config_flow.py
Normal file
198
homeassistant/components/google_weather/config_flow.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
"""Config flow for the Google Weather integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from google_weather_api import GoogleWeatherApi, GoogleWeatherApiError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import (
|
||||||
|
ConfigEntry,
|
||||||
|
ConfigEntryState,
|
||||||
|
ConfigFlow,
|
||||||
|
ConfigFlowResult,
|
||||||
|
ConfigSubentryFlow,
|
||||||
|
SubentryFlowResult,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_API_KEY,
|
||||||
|
CONF_LATITUDE,
|
||||||
|
CONF_LOCATION,
|
||||||
|
CONF_LONGITUDE,
|
||||||
|
CONF_NAME,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.data_entry_flow import section
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.selector import LocationSelector, LocationSelectorConfig
|
||||||
|
|
||||||
|
from .const import CONF_REFERRER, DOMAIN, SECTION_API_KEY_OPTIONS
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_API_KEY): str,
|
||||||
|
vol.Optional(SECTION_API_KEY_OPTIONS): section(
|
||||||
|
vol.Schema({vol.Optional(CONF_REFERRER): str}), {"collapsed": True}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _validate_input(
|
||||||
|
user_input: dict[str, Any],
|
||||||
|
api: GoogleWeatherApi,
|
||||||
|
errors: dict[str, str],
|
||||||
|
description_placeholders: dict[str, str],
|
||||||
|
) -> bool:
|
||||||
|
try:
|
||||||
|
await api.async_get_current_conditions(
|
||||||
|
latitude=user_input[CONF_LOCATION][CONF_LATITUDE],
|
||||||
|
longitude=user_input[CONF_LOCATION][CONF_LONGITUDE],
|
||||||
|
)
|
||||||
|
except GoogleWeatherApiError as err:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
description_placeholders["error_message"] = str(err)
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _get_location_schema(hass: HomeAssistant) -> vol.Schema:
|
||||||
|
"""Return the schema for a location with default values from the hass config."""
|
||||||
|
return vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_NAME, default=hass.config.location_name): str,
|
||||||
|
vol.Required(
|
||||||
|
CONF_LOCATION,
|
||||||
|
default={
|
||||||
|
CONF_LATITUDE: hass.config.latitude,
|
||||||
|
CONF_LONGITUDE: hass.config.longitude,
|
||||||
|
},
|
||||||
|
): LocationSelector(LocationSelectorConfig(radius=False)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_location_already_configured(
|
||||||
|
hass: HomeAssistant, new_data: dict[str, float], epsilon: float = 1e-4
|
||||||
|
) -> bool:
|
||||||
|
"""Check if the location is already configured."""
|
||||||
|
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||||
|
for subentry in entry.subentries.values():
|
||||||
|
# A more accurate way is to use the haversine formula, but for simplicity
|
||||||
|
# we use a simple distance check. The epsilon value is small anyway.
|
||||||
|
# This is mostly to capture cases where the user has slightly moved the location pin.
|
||||||
|
if (
|
||||||
|
abs(subentry.data[CONF_LATITUDE] - new_data[CONF_LATITUDE]) <= epsilon
|
||||||
|
and abs(subentry.data[CONF_LONGITUDE] - new_data[CONF_LONGITUDE])
|
||||||
|
<= epsilon
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleWeatherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Google Weather."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
description_placeholders: dict[str, str] = {
|
||||||
|
"api_key_url": "https://developers.google.com/maps/documentation/weather/get-api-key",
|
||||||
|
"restricting_api_keys_url": "https://developers.google.com/maps/api-security-best-practices#restricting-api-keys",
|
||||||
|
}
|
||||||
|
if user_input is not None:
|
||||||
|
api_key = user_input[CONF_API_KEY]
|
||||||
|
referrer = user_input.get(SECTION_API_KEY_OPTIONS, {}).get(CONF_REFERRER)
|
||||||
|
self._async_abort_entries_match({CONF_API_KEY: api_key})
|
||||||
|
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
|
api = GoogleWeatherApi(
|
||||||
|
session=async_get_clientsession(self.hass),
|
||||||
|
api_key=api_key,
|
||||||
|
referrer=referrer,
|
||||||
|
language_code=self.hass.config.language,
|
||||||
|
)
|
||||||
|
if await _validate_input(user_input, api, errors, description_placeholders):
|
||||||
|
return self.async_create_entry(
|
||||||
|
title="Google Weather",
|
||||||
|
data={
|
||||||
|
CONF_API_KEY: api_key,
|
||||||
|
CONF_REFERRER: referrer,
|
||||||
|
},
|
||||||
|
subentries=[
|
||||||
|
{
|
||||||
|
"subentry_type": "location",
|
||||||
|
"data": user_input[CONF_LOCATION],
|
||||||
|
"title": user_input[CONF_NAME],
|
||||||
|
"unique_id": None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
user_input = {}
|
||||||
|
schema = STEP_USER_DATA_SCHEMA.schema.copy()
|
||||||
|
schema.update(_get_location_schema(self.hass).schema)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
vol.Schema(schema), user_input
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders=description_placeholders,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@callback
|
||||||
|
def async_get_supported_subentry_types(
|
||||||
|
cls, config_entry: ConfigEntry
|
||||||
|
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||||
|
"""Return subentries supported by this integration."""
|
||||||
|
return {"location": LocationSubentryFlowHandler}
|
||||||
|
|
||||||
|
|
||||||
|
class LocationSubentryFlowHandler(ConfigSubentryFlow):
|
||||||
|
"""Handle a subentry flow for location."""
|
||||||
|
|
||||||
|
async def async_step_location(
|
||||||
|
self,
|
||||||
|
user_input: dict[str, Any] | None = None,
|
||||||
|
) -> SubentryFlowResult:
|
||||||
|
"""Handle the location step."""
|
||||||
|
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||||
|
return self.async_abort(reason="entry_not_loaded")
|
||||||
|
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
description_placeholders: dict[str, str] = {}
|
||||||
|
if user_input is not None:
|
||||||
|
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
|
api: GoogleWeatherApi = self._get_entry().runtime_data.api
|
||||||
|
if await _validate_input(user_input, api, errors, description_placeholders):
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_NAME],
|
||||||
|
data=user_input[CONF_LOCATION],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
user_input = {}
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="location",
|
||||||
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
_get_location_schema(self.hass), user_input
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders=description_placeholders,
|
||||||
|
)
|
||||||
|
|
||||||
|
async_step_user = async_step_location
|
||||||
8
homeassistant/components/google_weather/const.py
Normal file
8
homeassistant/components/google_weather/const.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"""Constants for the Google Weather integration."""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
DOMAIN = "google_weather"
|
||||||
|
|
||||||
|
SECTION_API_KEY_OPTIONS: Final = "api_key_options"
|
||||||
|
CONF_REFERRER: Final = "referrer"
|
||||||
169
homeassistant/components/google_weather/coordinator.py
Normal file
169
homeassistant/components/google_weather/coordinator.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
"""The Google Weather coordinator."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from google_weather_api import (
|
||||||
|
CurrentConditionsResponse,
|
||||||
|
DailyForecastResponse,
|
||||||
|
GoogleWeatherApi,
|
||||||
|
GoogleWeatherApiError,
|
||||||
|
HourlyForecastResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
|
||||||
|
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import (
|
||||||
|
TimestampDataUpdateCoordinator,
|
||||||
|
UpdateFailed,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
T = TypeVar(
|
||||||
|
"T",
|
||||||
|
bound=(
|
||||||
|
CurrentConditionsResponse
|
||||||
|
| DailyForecastResponse
|
||||||
|
| HourlyForecastResponse
|
||||||
|
| None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GoogleWeatherSubEntryRuntimeData:
|
||||||
|
"""Runtime data for a Google Weather sub-entry."""
|
||||||
|
|
||||||
|
coordinator_observation: GoogleWeatherCurrentConditionsCoordinator
|
||||||
|
coordinator_daily_forecast: GoogleWeatherDailyForecastCoordinator
|
||||||
|
coordinator_hourly_forecast: GoogleWeatherHourlyForecastCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GoogleWeatherRuntimeData:
|
||||||
|
"""Runtime data for the Google Weather integration."""
|
||||||
|
|
||||||
|
api: GoogleWeatherApi
|
||||||
|
subentries_runtime_data: dict[str, GoogleWeatherSubEntryRuntimeData]
|
||||||
|
|
||||||
|
|
||||||
|
type GoogleWeatherConfigEntry = ConfigEntry[GoogleWeatherRuntimeData]
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleWeatherBaseCoordinator(TimestampDataUpdateCoordinator[T]):
|
||||||
|
"""Base class for Google Weather coordinators."""
|
||||||
|
|
||||||
|
config_entry: GoogleWeatherConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: GoogleWeatherConfigEntry,
|
||||||
|
subentry: ConfigSubentry,
|
||||||
|
data_type_name: str,
|
||||||
|
update_interval: timedelta,
|
||||||
|
api_method: Callable[..., Awaitable[T]],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the data updater."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name=f"Google Weather {data_type_name} coordinator for {subentry.title}",
|
||||||
|
update_interval=update_interval,
|
||||||
|
)
|
||||||
|
self.subentry = subentry
|
||||||
|
self._data_type_name = data_type_name
|
||||||
|
self._api_method = api_method
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> T:
|
||||||
|
"""Fetch data from API and handle errors."""
|
||||||
|
try:
|
||||||
|
return await self._api_method(
|
||||||
|
self.subentry.data[CONF_LATITUDE],
|
||||||
|
self.subentry.data[CONF_LONGITUDE],
|
||||||
|
)
|
||||||
|
except GoogleWeatherApiError as err:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Error fetching %s for %s: %s",
|
||||||
|
self._data_type_name,
|
||||||
|
self.subentry.title,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
raise UpdateFailed(f"Error fetching {self._data_type_name}") from err
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleWeatherCurrentConditionsCoordinator(
|
||||||
|
GoogleWeatherBaseCoordinator[CurrentConditionsResponse]
|
||||||
|
):
|
||||||
|
"""Handle fetching current weather conditions."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: GoogleWeatherConfigEntry,
|
||||||
|
subentry: ConfigSubentry,
|
||||||
|
api: GoogleWeatherApi,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the data updater."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
config_entry,
|
||||||
|
subentry,
|
||||||
|
"current weather conditions",
|
||||||
|
timedelta(minutes=15),
|
||||||
|
api.async_get_current_conditions,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleWeatherDailyForecastCoordinator(
|
||||||
|
GoogleWeatherBaseCoordinator[DailyForecastResponse]
|
||||||
|
):
|
||||||
|
"""Handle fetching daily weather forecast."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: GoogleWeatherConfigEntry,
|
||||||
|
subentry: ConfigSubentry,
|
||||||
|
api: GoogleWeatherApi,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the data updater."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
config_entry,
|
||||||
|
subentry,
|
||||||
|
"daily weather forecast",
|
||||||
|
timedelta(hours=1),
|
||||||
|
api.async_get_daily_forecast,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleWeatherHourlyForecastCoordinator(
|
||||||
|
GoogleWeatherBaseCoordinator[HourlyForecastResponse]
|
||||||
|
):
|
||||||
|
"""Handle fetching hourly weather forecast."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: GoogleWeatherConfigEntry,
|
||||||
|
subentry: ConfigSubentry,
|
||||||
|
api: GoogleWeatherApi,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the data updater."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
config_entry,
|
||||||
|
subentry,
|
||||||
|
"hourly weather forecast",
|
||||||
|
timedelta(hours=1),
|
||||||
|
api.async_get_hourly_forecast,
|
||||||
|
)
|
||||||
28
homeassistant/components/google_weather/entity.py
Normal file
28
homeassistant/components/google_weather/entity.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""Base entity for Google Weather."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigSubentry
|
||||||
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import GoogleWeatherConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleWeatherBaseEntity(Entity):
|
||||||
|
"""Base entity for all Google Weather entities."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, config_entry: GoogleWeatherConfigEntry, subentry: ConfigSubentry
|
||||||
|
) -> None:
|
||||||
|
"""Initialize base entity."""
|
||||||
|
self._attr_unique_id = subentry.subentry_id
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, subentry.subentry_id)},
|
||||||
|
name=subentry.title,
|
||||||
|
manufacturer="Google",
|
||||||
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
|
)
|
||||||
12
homeassistant/components/google_weather/manifest.json
Normal file
12
homeassistant/components/google_weather/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"domain": "google_weather",
|
||||||
|
"name": "Google Weather",
|
||||||
|
"codeowners": ["@tronikos"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/google_weather",
|
||||||
|
"integration_type": "service",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"loggers": ["google_weather_api"],
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["python-google-weather-api==0.0.4"]
|
||||||
|
}
|
||||||
82
homeassistant/components/google_weather/quality_scale.yaml
Normal file
82
homeassistant/components/google_weather/quality_scale.yaml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: No actions.
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: No actions.
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: No events subscribed.
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure: done
|
||||||
|
test-before-setup: done
|
||||||
|
unique-config-entry: done
|
||||||
|
|
||||||
|
# Silver
|
||||||
|
action-exceptions:
|
||||||
|
status: exempt
|
||||||
|
comment: No actions.
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters:
|
||||||
|
status: exempt
|
||||||
|
comment: No configuration options.
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable: done
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable: done
|
||||||
|
parallel-updates: done
|
||||||
|
reauthentication-flow: todo
|
||||||
|
test-coverage: done
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info:
|
||||||
|
status: exempt
|
||||||
|
comment: No discovery.
|
||||||
|
discovery:
|
||||||
|
status: exempt
|
||||||
|
comment: No discovery.
|
||||||
|
docs-data-update: done
|
||||||
|
docs-examples: done
|
||||||
|
docs-known-limitations: done
|
||||||
|
docs-supported-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: No physical devices.
|
||||||
|
docs-supported-functions: done
|
||||||
|
docs-troubleshooting: done
|
||||||
|
docs-use-cases: done
|
||||||
|
dynamic-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: N/A
|
||||||
|
entity-category: done
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default: done
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: todo
|
||||||
|
icon-translations: done
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues:
|
||||||
|
status: exempt
|
||||||
|
comment: No repairs.
|
||||||
|
stale-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: N/A
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: done
|
||||||
65
homeassistant/components/google_weather/strings.json
Normal file
65
homeassistant/components/google_weather/strings.json
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Unable to connect to the Google Weather API:\n\n{error_message}",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||||
|
"location": "[%key:common::config_flow::data::location%]",
|
||||||
|
"name": "[%key:common::config_flow::data::name%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"api_key": "A unique alphanumeric string that associates your Google billing account with Google Weather API",
|
||||||
|
"location": "Location coordinates",
|
||||||
|
"name": "Location name"
|
||||||
|
},
|
||||||
|
"description": "Get your API key from [here]({api_key_url}).",
|
||||||
|
"sections": {
|
||||||
|
"api_key_options": {
|
||||||
|
"data": {
|
||||||
|
"referrer": "HTTP referrer"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"referrer": "Specify this only if the API key has a [website application restriction]({restricting_api_keys_url})"
|
||||||
|
},
|
||||||
|
"name": "Optional API key options"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config_subentries": {
|
||||||
|
"location": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
|
||||||
|
"entry_not_loaded": "Cannot add things while the configuration is disabled."
|
||||||
|
},
|
||||||
|
"entry_type": "Location",
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:component::google_weather::config::error::cannot_connect%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"initiate_flow": {
|
||||||
|
"user": "Add location"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"location": {
|
||||||
|
"data": {
|
||||||
|
"location": "[%key:common::config_flow::data::location%]",
|
||||||
|
"name": "[%key:common::config_flow::data::name%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"location": "[%key:component::google_weather::config::step::user::data_description::location%]",
|
||||||
|
"name": "[%key:component::google_weather::config::step::user::data_description::name%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
366
homeassistant/components/google_weather/weather.py
Normal file
366
homeassistant/components/google_weather/weather.py
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
"""Weather entity."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from google_weather_api import (
|
||||||
|
DailyForecastResponse,
|
||||||
|
HourlyForecastResponse,
|
||||||
|
WeatherCondition,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.weather import (
|
||||||
|
ATTR_CONDITION_CLEAR_NIGHT,
|
||||||
|
ATTR_CONDITION_CLOUDY,
|
||||||
|
ATTR_CONDITION_HAIL,
|
||||||
|
ATTR_CONDITION_LIGHTNING_RAINY,
|
||||||
|
ATTR_CONDITION_PARTLYCLOUDY,
|
||||||
|
ATTR_CONDITION_POURING,
|
||||||
|
ATTR_CONDITION_RAINY,
|
||||||
|
ATTR_CONDITION_SNOWY,
|
||||||
|
ATTR_CONDITION_SNOWY_RAINY,
|
||||||
|
ATTR_CONDITION_SUNNY,
|
||||||
|
ATTR_CONDITION_WINDY,
|
||||||
|
ATTR_FORECAST_CLOUD_COVERAGE,
|
||||||
|
ATTR_FORECAST_CONDITION,
|
||||||
|
ATTR_FORECAST_HUMIDITY,
|
||||||
|
ATTR_FORECAST_IS_DAYTIME,
|
||||||
|
ATTR_FORECAST_NATIVE_APPARENT_TEMP,
|
||||||
|
ATTR_FORECAST_NATIVE_DEW_POINT,
|
||||||
|
ATTR_FORECAST_NATIVE_PRECIPITATION,
|
||||||
|
ATTR_FORECAST_NATIVE_PRESSURE,
|
||||||
|
ATTR_FORECAST_NATIVE_TEMP,
|
||||||
|
ATTR_FORECAST_NATIVE_TEMP_LOW,
|
||||||
|
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
|
||||||
|
ATTR_FORECAST_NATIVE_WIND_SPEED,
|
||||||
|
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||||
|
ATTR_FORECAST_TIME,
|
||||||
|
ATTR_FORECAST_UV_INDEX,
|
||||||
|
ATTR_FORECAST_WIND_BEARING,
|
||||||
|
CoordinatorWeatherEntity,
|
||||||
|
Forecast,
|
||||||
|
WeatherEntityFeature,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigSubentry
|
||||||
|
from homeassistant.const import (
|
||||||
|
UnitOfLength,
|
||||||
|
UnitOfPrecipitationDepth,
|
||||||
|
UnitOfPressure,
|
||||||
|
UnitOfSpeed,
|
||||||
|
UnitOfTemperature,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import (
|
||||||
|
GoogleWeatherConfigEntry,
|
||||||
|
GoogleWeatherCurrentConditionsCoordinator,
|
||||||
|
GoogleWeatherDailyForecastCoordinator,
|
||||||
|
GoogleWeatherHourlyForecastCoordinator,
|
||||||
|
)
|
||||||
|
from .entity import GoogleWeatherBaseEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
# Maps https://developers.google.com/maps/documentation/weather/weather-condition-icons
|
||||||
|
# to https://developers.home-assistant.io/docs/core/entity/weather/#recommended-values-for-state-and-condition
|
||||||
|
_CONDITION_MAP: dict[WeatherCondition.Type, str | None] = {
|
||||||
|
WeatherCondition.Type.TYPE_UNSPECIFIED: None,
|
||||||
|
WeatherCondition.Type.CLEAR: ATTR_CONDITION_SUNNY,
|
||||||
|
WeatherCondition.Type.MOSTLY_CLEAR: ATTR_CONDITION_PARTLYCLOUDY,
|
||||||
|
WeatherCondition.Type.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY,
|
||||||
|
WeatherCondition.Type.MOSTLY_CLOUDY: ATTR_CONDITION_CLOUDY,
|
||||||
|
WeatherCondition.Type.CLOUDY: ATTR_CONDITION_CLOUDY,
|
||||||
|
WeatherCondition.Type.WINDY: ATTR_CONDITION_WINDY,
|
||||||
|
WeatherCondition.Type.WIND_AND_RAIN: ATTR_CONDITION_RAINY,
|
||||||
|
WeatherCondition.Type.LIGHT_RAIN_SHOWERS: ATTR_CONDITION_RAINY,
|
||||||
|
WeatherCondition.Type.CHANCE_OF_SHOWERS: ATTR_CONDITION_RAINY,
|
||||||
|
WeatherCondition.Type.SCATTERED_SHOWERS: ATTR_CONDITION_RAINY,
|
||||||
|
WeatherCondition.Type.RAIN_SHOWERS: ATTR_CONDITION_RAINY,
|
||||||
|
WeatherCondition.Type.HEAVY_RAIN_SHOWERS: ATTR_CONDITION_POURING,
|
||||||
|
WeatherCondition.Type.LIGHT_TO_MODERATE_RAIN: ATTR_CONDITION_RAINY,
|
||||||
|
WeatherCondition.Type.MODERATE_TO_HEAVY_RAIN: ATTR_CONDITION_POURING,
|
||||||
|
WeatherCondition.Type.RAIN: ATTR_CONDITION_RAINY,
|
||||||
|
WeatherCondition.Type.LIGHT_RAIN: ATTR_CONDITION_RAINY,
|
||||||
|
WeatherCondition.Type.HEAVY_RAIN: ATTR_CONDITION_POURING,
|
||||||
|
WeatherCondition.Type.RAIN_PERIODICALLY_HEAVY: ATTR_CONDITION_POURING,
|
||||||
|
WeatherCondition.Type.LIGHT_SNOW_SHOWERS: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.CHANCE_OF_SNOW_SHOWERS: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.SCATTERED_SNOW_SHOWERS: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.SNOW_SHOWERS: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.HEAVY_SNOW_SHOWERS: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.LIGHT_TO_MODERATE_SNOW: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.MODERATE_TO_HEAVY_SNOW: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.SNOW: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.LIGHT_SNOW: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.HEAVY_SNOW: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.SNOWSTORM: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.SNOW_PERIODICALLY_HEAVY: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.HEAVY_SNOW_STORM: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.BLOWING_SNOW: ATTR_CONDITION_SNOWY,
|
||||||
|
WeatherCondition.Type.RAIN_AND_SNOW: ATTR_CONDITION_SNOWY_RAINY,
|
||||||
|
WeatherCondition.Type.HAIL: ATTR_CONDITION_HAIL,
|
||||||
|
WeatherCondition.Type.HAIL_SHOWERS: ATTR_CONDITION_HAIL,
|
||||||
|
WeatherCondition.Type.THUNDERSTORM: ATTR_CONDITION_LIGHTNING_RAINY,
|
||||||
|
WeatherCondition.Type.THUNDERSHOWER: ATTR_CONDITION_LIGHTNING_RAINY,
|
||||||
|
WeatherCondition.Type.LIGHT_THUNDERSTORM_RAIN: ATTR_CONDITION_LIGHTNING_RAINY,
|
||||||
|
WeatherCondition.Type.SCATTERED_THUNDERSTORMS: ATTR_CONDITION_LIGHTNING_RAINY,
|
||||||
|
WeatherCondition.Type.HEAVY_THUNDERSTORM: ATTR_CONDITION_LIGHTNING_RAINY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_condition(
|
||||||
|
api_condition: WeatherCondition.Type, is_daytime: bool
|
||||||
|
) -> str | None:
|
||||||
|
"""Map Google Weather condition to Home Assistant condition."""
|
||||||
|
cond = _CONDITION_MAP[api_condition]
|
||||||
|
if cond == ATTR_CONDITION_SUNNY and not is_daytime:
|
||||||
|
return ATTR_CONDITION_CLEAR_NIGHT
|
||||||
|
return cond
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: GoogleWeatherConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Add a weather entity from a config_entry."""
|
||||||
|
for subentry in entry.subentries.values():
|
||||||
|
async_add_entities(
|
||||||
|
[GoogleWeatherEntity(entry, subentry)],
|
||||||
|
config_subentry_id=subentry.subentry_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleWeatherEntity(
|
||||||
|
CoordinatorWeatherEntity[
|
||||||
|
GoogleWeatherCurrentConditionsCoordinator,
|
||||||
|
GoogleWeatherDailyForecastCoordinator,
|
||||||
|
GoogleWeatherHourlyForecastCoordinator,
|
||||||
|
GoogleWeatherDailyForecastCoordinator,
|
||||||
|
],
|
||||||
|
GoogleWeatherBaseEntity,
|
||||||
|
):
|
||||||
|
"""Representation of a Google Weather entity."""
|
||||||
|
|
||||||
|
_attr_attribution = "Data from Google Weather"
|
||||||
|
|
||||||
|
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
|
_attr_native_pressure_unit = UnitOfPressure.MBAR
|
||||||
|
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
||||||
|
_attr_native_visibility_unit = UnitOfLength.KILOMETERS
|
||||||
|
_attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
|
||||||
|
|
||||||
|
_attr_name = None
|
||||||
|
|
||||||
|
_attr_supported_features = (
|
||||||
|
WeatherEntityFeature.FORECAST_DAILY
|
||||||
|
| WeatherEntityFeature.FORECAST_HOURLY
|
||||||
|
| WeatherEntityFeature.FORECAST_TWICE_DAILY
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
entry: GoogleWeatherConfigEntry,
|
||||||
|
subentry: ConfigSubentry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the weather entity."""
|
||||||
|
subentry_runtime_data = entry.runtime_data.subentries_runtime_data[
|
||||||
|
subentry.subentry_id
|
||||||
|
]
|
||||||
|
super().__init__(
|
||||||
|
observation_coordinator=subentry_runtime_data.coordinator_observation,
|
||||||
|
daily_coordinator=subentry_runtime_data.coordinator_daily_forecast,
|
||||||
|
hourly_coordinator=subentry_runtime_data.coordinator_hourly_forecast,
|
||||||
|
twice_daily_coordinator=subentry_runtime_data.coordinator_daily_forecast,
|
||||||
|
)
|
||||||
|
GoogleWeatherBaseEntity.__init__(self, entry, subentry)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def condition(self) -> str | None:
|
||||||
|
"""Return the current condition."""
|
||||||
|
return _get_condition(
|
||||||
|
self.coordinator.data.weather_condition.type,
|
||||||
|
self.coordinator.data.is_daytime,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_temperature(self) -> float:
|
||||||
|
"""Return the temperature."""
|
||||||
|
return self.coordinator.data.temperature.degrees
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_apparent_temperature(self) -> float:
|
||||||
|
"""Return the apparent temperature."""
|
||||||
|
return self.coordinator.data.feels_like_temperature.degrees
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_dew_point(self) -> float:
|
||||||
|
"""Return the dew point."""
|
||||||
|
return self.coordinator.data.dew_point.degrees
|
||||||
|
|
||||||
|
@property
|
||||||
|
def humidity(self) -> int:
|
||||||
|
"""Return the humidity."""
|
||||||
|
return self.coordinator.data.relative_humidity
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uv_index(self) -> float:
|
||||||
|
"""Return the UV index."""
|
||||||
|
return float(self.coordinator.data.uv_index)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_pressure(self) -> float:
|
||||||
|
"""Return the pressure."""
|
||||||
|
return self.coordinator.data.air_pressure.mean_sea_level_millibars
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_wind_gust_speed(self) -> float:
|
||||||
|
"""Return the wind gust speed."""
|
||||||
|
return self.coordinator.data.wind.gust.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_wind_speed(self) -> float:
|
||||||
|
"""Return the wind speed."""
|
||||||
|
return self.coordinator.data.wind.speed.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wind_bearing(self) -> int:
|
||||||
|
"""Return the wind bearing."""
|
||||||
|
return self.coordinator.data.wind.direction.degrees
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_visibility(self) -> float:
|
||||||
|
"""Return the visibility."""
|
||||||
|
return self.coordinator.data.visibility.distance
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cloud_coverage(self) -> float:
|
||||||
|
"""Return the Cloud coverage in %."""
|
||||||
|
return float(self.coordinator.data.cloud_cover)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_forecast_daily(self) -> list[Forecast] | None:
|
||||||
|
"""Return the daily forecast in native units."""
|
||||||
|
coordinator = self.forecast_coordinators["daily"]
|
||||||
|
assert coordinator
|
||||||
|
daily_data = coordinator.data
|
||||||
|
assert isinstance(daily_data, DailyForecastResponse)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
ATTR_FORECAST_CONDITION: _get_condition(
|
||||||
|
item.daytime_forecast.weather_condition.type, is_daytime=True
|
||||||
|
),
|
||||||
|
ATTR_FORECAST_TIME: item.interval.start_time,
|
||||||
|
ATTR_FORECAST_HUMIDITY: item.daytime_forecast.relative_humidity,
|
||||||
|
ATTR_FORECAST_PRECIPITATION_PROBABILITY: max(
|
||||||
|
item.daytime_forecast.precipitation.probability.percent,
|
||||||
|
item.nighttime_forecast.precipitation.probability.percent,
|
||||||
|
),
|
||||||
|
ATTR_FORECAST_CLOUD_COVERAGE: item.daytime_forecast.cloud_cover,
|
||||||
|
ATTR_FORECAST_NATIVE_PRECIPITATION: (
|
||||||
|
item.daytime_forecast.precipitation.qpf.quantity
|
||||||
|
+ item.nighttime_forecast.precipitation.qpf.quantity
|
||||||
|
),
|
||||||
|
ATTR_FORECAST_NATIVE_TEMP: item.max_temperature.degrees,
|
||||||
|
ATTR_FORECAST_NATIVE_TEMP_LOW: item.min_temperature.degrees,
|
||||||
|
ATTR_FORECAST_NATIVE_APPARENT_TEMP: (
|
||||||
|
item.feels_like_max_temperature.degrees
|
||||||
|
),
|
||||||
|
ATTR_FORECAST_WIND_BEARING: item.daytime_forecast.wind.direction.degrees,
|
||||||
|
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: max(
|
||||||
|
item.daytime_forecast.wind.gust.value,
|
||||||
|
item.nighttime_forecast.wind.gust.value,
|
||||||
|
),
|
||||||
|
ATTR_FORECAST_NATIVE_WIND_SPEED: max(
|
||||||
|
item.daytime_forecast.wind.speed.value,
|
||||||
|
item.nighttime_forecast.wind.speed.value,
|
||||||
|
),
|
||||||
|
ATTR_FORECAST_UV_INDEX: item.daytime_forecast.uv_index,
|
||||||
|
}
|
||||||
|
for item in daily_data.forecast_days
|
||||||
|
]
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_forecast_hourly(self) -> list[Forecast] | None:
|
||||||
|
"""Return the hourly forecast in native units."""
|
||||||
|
coordinator = self.forecast_coordinators["hourly"]
|
||||||
|
assert coordinator
|
||||||
|
hourly_data = coordinator.data
|
||||||
|
assert isinstance(hourly_data, HourlyForecastResponse)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
ATTR_FORECAST_CONDITION: _get_condition(
|
||||||
|
item.weather_condition.type, item.is_daytime
|
||||||
|
),
|
||||||
|
ATTR_FORECAST_TIME: item.interval.start_time,
|
||||||
|
ATTR_FORECAST_HUMIDITY: item.relative_humidity,
|
||||||
|
ATTR_FORECAST_PRECIPITATION_PROBABILITY: item.precipitation.probability.percent,
|
||||||
|
ATTR_FORECAST_CLOUD_COVERAGE: item.cloud_cover,
|
||||||
|
ATTR_FORECAST_NATIVE_PRECIPITATION: item.precipitation.qpf.quantity,
|
||||||
|
ATTR_FORECAST_NATIVE_PRESSURE: item.air_pressure.mean_sea_level_millibars,
|
||||||
|
ATTR_FORECAST_NATIVE_TEMP: item.temperature.degrees,
|
||||||
|
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item.feels_like_temperature.degrees,
|
||||||
|
ATTR_FORECAST_WIND_BEARING: item.wind.direction.degrees,
|
||||||
|
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: item.wind.gust.value,
|
||||||
|
ATTR_FORECAST_NATIVE_WIND_SPEED: item.wind.speed.value,
|
||||||
|
ATTR_FORECAST_NATIVE_DEW_POINT: item.dew_point.degrees,
|
||||||
|
ATTR_FORECAST_UV_INDEX: item.uv_index,
|
||||||
|
ATTR_FORECAST_IS_DAYTIME: item.is_daytime,
|
||||||
|
}
|
||||||
|
for item in hourly_data.forecast_hours
|
||||||
|
]
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_forecast_twice_daily(self) -> list[Forecast] | None:
|
||||||
|
"""Return the twice daily forecast in native units."""
|
||||||
|
coordinator = self.forecast_coordinators["twice_daily"]
|
||||||
|
assert coordinator
|
||||||
|
daily_data = coordinator.data
|
||||||
|
assert isinstance(daily_data, DailyForecastResponse)
|
||||||
|
forecasts: list[Forecast] = []
|
||||||
|
for item in daily_data.forecast_days:
|
||||||
|
# Process daytime forecast
|
||||||
|
day_forecast = item.daytime_forecast
|
||||||
|
forecasts.append(
|
||||||
|
{
|
||||||
|
ATTR_FORECAST_CONDITION: _get_condition(
|
||||||
|
day_forecast.weather_condition.type, is_daytime=True
|
||||||
|
),
|
||||||
|
ATTR_FORECAST_TIME: day_forecast.interval.start_time,
|
||||||
|
ATTR_FORECAST_HUMIDITY: day_forecast.relative_humidity,
|
||||||
|
ATTR_FORECAST_PRECIPITATION_PROBABILITY: day_forecast.precipitation.probability.percent,
|
||||||
|
ATTR_FORECAST_CLOUD_COVERAGE: day_forecast.cloud_cover,
|
||||||
|
ATTR_FORECAST_NATIVE_PRECIPITATION: day_forecast.precipitation.qpf.quantity,
|
||||||
|
ATTR_FORECAST_NATIVE_TEMP: item.max_temperature.degrees,
|
||||||
|
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item.feels_like_max_temperature.degrees,
|
||||||
|
ATTR_FORECAST_WIND_BEARING: day_forecast.wind.direction.degrees,
|
||||||
|
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: day_forecast.wind.gust.value,
|
||||||
|
ATTR_FORECAST_NATIVE_WIND_SPEED: day_forecast.wind.speed.value,
|
||||||
|
ATTR_FORECAST_UV_INDEX: day_forecast.uv_index,
|
||||||
|
ATTR_FORECAST_IS_DAYTIME: True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process nighttime forecast
|
||||||
|
night_forecast = item.nighttime_forecast
|
||||||
|
forecasts.append(
|
||||||
|
{
|
||||||
|
ATTR_FORECAST_CONDITION: _get_condition(
|
||||||
|
night_forecast.weather_condition.type, is_daytime=False
|
||||||
|
),
|
||||||
|
ATTR_FORECAST_TIME: night_forecast.interval.start_time,
|
||||||
|
ATTR_FORECAST_HUMIDITY: night_forecast.relative_humidity,
|
||||||
|
ATTR_FORECAST_PRECIPITATION_PROBABILITY: night_forecast.precipitation.probability.percent,
|
||||||
|
ATTR_FORECAST_CLOUD_COVERAGE: night_forecast.cloud_cover,
|
||||||
|
ATTR_FORECAST_NATIVE_PRECIPITATION: night_forecast.precipitation.qpf.quantity,
|
||||||
|
ATTR_FORECAST_NATIVE_TEMP: item.min_temperature.degrees,
|
||||||
|
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item.feels_like_min_temperature.degrees,
|
||||||
|
ATTR_FORECAST_WIND_BEARING: night_forecast.wind.direction.degrees,
|
||||||
|
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: night_forecast.wind.gust.value,
|
||||||
|
ATTR_FORECAST_NATIVE_WIND_SPEED: night_forecast.wind.speed.value,
|
||||||
|
ATTR_FORECAST_UV_INDEX: night_forecast.uv_index,
|
||||||
|
ATTR_FORECAST_IS_DAYTIME: False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return forecasts
|
||||||
@@ -7,7 +7,7 @@ from collections import defaultdict
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
from typing import Any
|
||||||
|
|
||||||
from aiohomeconnect.client import Client as HomeConnectClient
|
from aiohomeconnect.client import Client as HomeConnectClient
|
||||||
from aiohomeconnect.model import (
|
from aiohomeconnect.model import (
|
||||||
@@ -247,14 +247,15 @@ class HomeConnectCoordinator(
|
|||||||
value=event.value,
|
value=event.value,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
event_value = event.value
|
||||||
if event_key in (
|
if event_key in (
|
||||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||||
):
|
) and isinstance(event_value, str):
|
||||||
await self.update_options(
|
await self.update_options(
|
||||||
event_message_ha_id,
|
event_message_ha_id,
|
||||||
event_key,
|
event_key,
|
||||||
ProgramKey(cast(str, event.value)),
|
ProgramKey(event_value),
|
||||||
)
|
)
|
||||||
events[event_key] = event
|
events[event_key] = event
|
||||||
self._call_event_listener(event_message)
|
self._call_event_listener(event_message)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from aiohomeconnect.model.error import (
|
|||||||
TooManyRequestsError,
|
TooManyRequestsError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.const import STATE_UNAVAILABLE
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
@@ -62,10 +61,8 @@ class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
|
|||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Handle updated data from the coordinator."""
|
"""Handle updated data from the coordinator."""
|
||||||
self.update_native_value()
|
self.update_native_value()
|
||||||
available = self._attr_available = self.appliance.info.connected
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
state = STATE_UNAVAILABLE if not available else self.state
|
_LOGGER.debug("Updated %s", self)
|
||||||
_LOGGER.debug("Updated %s, new state: %s", self.entity_id, state)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bsh_key(self) -> str:
|
def bsh_key(self) -> str:
|
||||||
@@ -80,7 +77,7 @@ class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
|
|||||||
as event updates should take precedence over the coordinator
|
as event updates should take precedence over the coordinator
|
||||||
refresh.
|
refresh.
|
||||||
"""
|
"""
|
||||||
return self._attr_available
|
return self.appliance.info.connected and self._attr_available
|
||||||
|
|
||||||
|
|
||||||
class HomeConnectOptionEntity(HomeConnectEntity):
|
class HomeConnectOptionEntity(HomeConnectEntity):
|
||||||
|
|||||||
@@ -22,6 +22,6 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aiohomeconnect"],
|
"loggers": ["aiohomeconnect"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aiohomeconnect==0.23.0"],
|
"requirements": ["aiohomeconnect==0.23.1"],
|
||||||
"zeroconf": ["_homeconnect._tcp.local."]
|
"zeroconf": ["_homeconnect._tcp.local."]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -412,8 +412,8 @@ class HomeConnectProgramSelectEntity(HomeConnectEntity, SelectEntity):
|
|||||||
"""Set the program value."""
|
"""Set the program value."""
|
||||||
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
|
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
|
||||||
self._attr_current_option = (
|
self._attr_current_option = (
|
||||||
PROGRAMS_TRANSLATION_KEYS_MAP.get(cast(ProgramKey, event.value))
|
PROGRAMS_TRANSLATION_KEYS_MAP.get(ProgramKey(event_value))
|
||||||
if event
|
if event and isinstance(event_value := event.value, str)
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -556,8 +556,11 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
|
|||||||
status = self.appliance.status[cast(StatusKey, self.bsh_key)].value
|
status = self.appliance.status[cast(StatusKey, self.bsh_key)].value
|
||||||
self._update_native_value(status)
|
self._update_native_value(status)
|
||||||
|
|
||||||
def _update_native_value(self, status: str | float) -> None:
|
def _update_native_value(self, status: str | float | None) -> None:
|
||||||
"""Set the value of the sensor based on the given value."""
|
"""Set the value of the sensor based on the given value."""
|
||||||
|
if status is None:
|
||||||
|
self._attr_native_value = None
|
||||||
|
return
|
||||||
match self.device_class:
|
match self.device_class:
|
||||||
case SensorDeviceClass.TIMESTAMP:
|
case SensorDeviceClass.TIMESTAMP:
|
||||||
self._attr_native_value = dt_util.utcnow() + timedelta(
|
self._attr_native_value = dt_util.utcnow() + timedelta(
|
||||||
|
|||||||
@@ -76,9 +76,18 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
|||||||
"""Mixin for Home Assistant Connect ZBT-2 firmware methods."""
|
"""Mixin for Home Assistant Connect ZBT-2 firmware methods."""
|
||||||
|
|
||||||
context: ConfigFlowContext
|
context: ConfigFlowContext
|
||||||
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR]
|
|
||||||
ZIGBEE_BAUDRATE = 460800
|
ZIGBEE_BAUDRATE = 460800
|
||||||
|
|
||||||
|
# Early ZBT-2 samples used RTS/DTR to trigger the bootloader, later ones use the
|
||||||
|
# baudrate method. Since the two are mutually exclusive we just use both.
|
||||||
|
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE]
|
||||||
|
APPLICATION_PROBE_METHODS = [
|
||||||
|
(ApplicationType.GECKO_BOOTLOADER, 115200),
|
||||||
|
(ApplicationType.EZSP, ZIGBEE_BAUDRATE),
|
||||||
|
(ApplicationType.SPINEL, 460800),
|
||||||
|
]
|
||||||
|
|
||||||
async def async_step_install_zigbee_firmware(
|
async def async_step_install_zigbee_firmware(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
"dependencies": ["hardware", "usb", "homeassistant_hardware"],
|
"dependencies": ["hardware", "usb", "homeassistant_hardware"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_connect_zbt2",
|
"documentation": "https://www.home-assistant.io/integrations/homeassistant_connect_zbt2",
|
||||||
"integration_type": "hardware",
|
"integration_type": "hardware",
|
||||||
|
"loggers": [
|
||||||
|
"bellows",
|
||||||
|
"universal_silabs_flasher",
|
||||||
|
"zigpy.serial",
|
||||||
|
"serial_asyncio_fast"
|
||||||
|
],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"usb": [
|
"usb": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from homeassistant.components.homeassistant_hardware.update import (
|
|||||||
from homeassistant.components.homeassistant_hardware.util import (
|
from homeassistant.components.homeassistant_hardware.util import (
|
||||||
ApplicationType,
|
ApplicationType,
|
||||||
FirmwareInfo,
|
FirmwareInfo,
|
||||||
ResetTarget,
|
|
||||||
)
|
)
|
||||||
from homeassistant.components.update import UpdateDeviceClass
|
from homeassistant.components.update import UpdateDeviceClass
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
@@ -24,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import HomeAssistantConnectZBT2ConfigEntry
|
from . import HomeAssistantConnectZBT2ConfigEntry
|
||||||
|
from .config_flow import ZBT2FirmwareMixin
|
||||||
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, HARDWARE_NAME, SERIAL_NUMBER
|
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, HARDWARE_NAME, SERIAL_NUMBER
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -134,7 +134,8 @@ async def async_setup_entry(
|
|||||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||||
"""Connect ZBT-2 firmware update entity."""
|
"""Connect ZBT-2 firmware update entity."""
|
||||||
|
|
||||||
bootloader_reset_methods = [ResetTarget.RTS_DTR]
|
BOOTLOADER_RESET_METHODS = ZBT2FirmwareMixin.BOOTLOADER_RESET_METHODS
|
||||||
|
APPLICATION_PROBE_METHODS = ZBT2FirmwareMixin.APPLICATION_PROBE_METHODS
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
|||||||
|
|
||||||
ZIGBEE_BAUDRATE = 115200 # Default, subclasses may override
|
ZIGBEE_BAUDRATE = 115200 # Default, subclasses may override
|
||||||
BOOTLOADER_RESET_METHODS: list[ResetTarget] = [] # Default, subclasses may override
|
BOOTLOADER_RESET_METHODS: list[ResetTarget] = [] # Default, subclasses may override
|
||||||
|
APPLICATION_PROBE_METHODS: list[tuple[ApplicationType, int]] = []
|
||||||
|
|
||||||
_picked_firmware_type: PickedFirmwareType
|
_picked_firmware_type: PickedFirmwareType
|
||||||
_zigbee_flow_strategy: ZigbeeFlowStrategy = ZigbeeFlowStrategy.RECOMMENDED
|
_zigbee_flow_strategy: ZigbeeFlowStrategy = ZigbeeFlowStrategy.RECOMMENDED
|
||||||
@@ -230,7 +231,11 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
|||||||
# Installing new firmware is only truly required if the wrong type is
|
# Installing new firmware is only truly required if the wrong type is
|
||||||
# installed: upgrading to the latest release of the current firmware type
|
# installed: upgrading to the latest release of the current firmware type
|
||||||
# isn't strictly necessary for functionality.
|
# isn't strictly necessary for functionality.
|
||||||
self._probed_firmware_info = await probe_silabs_firmware_info(self._device)
|
self._probed_firmware_info = await probe_silabs_firmware_info(
|
||||||
|
self._device,
|
||||||
|
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||||
|
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||||
|
)
|
||||||
|
|
||||||
firmware_install_required = self._probed_firmware_info is None or (
|
firmware_install_required = self._probed_firmware_info is None or (
|
||||||
self._probed_firmware_info.firmware_type != expected_installed_firmware_type
|
self._probed_firmware_info.firmware_type != expected_installed_firmware_type
|
||||||
@@ -295,6 +300,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
|||||||
fw_data=fw_data,
|
fw_data=fw_data,
|
||||||
expected_installed_firmware_type=expected_installed_firmware_type,
|
expected_installed_firmware_type=expected_installed_firmware_type,
|
||||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||||
|
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||||
progress_callback=lambda offset, total: self.async_update_progress(
|
progress_callback=lambda offset, total: self.async_update_progress(
|
||||||
offset / total
|
offset / total
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"universal-silabs-flasher==0.0.37",
|
"universal-silabs-flasher==0.1.0",
|
||||||
"ha-silabs-firmware-client==0.3.0"
|
"ha-silabs-firmware-client==0.3.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ class BaseFirmwareUpdateEntity(
|
|||||||
|
|
||||||
# Subclasses provide the mapping between firmware types and entity descriptions
|
# Subclasses provide the mapping between firmware types and entity descriptions
|
||||||
entity_description: FirmwareUpdateEntityDescription
|
entity_description: FirmwareUpdateEntityDescription
|
||||||
bootloader_reset_methods: list[ResetTarget] = []
|
BOOTLOADER_RESET_METHODS: list[ResetTarget]
|
||||||
|
APPLICATION_PROBE_METHODS: list[tuple[ApplicationType, int]]
|
||||||
|
|
||||||
_attr_supported_features = (
|
_attr_supported_features = (
|
||||||
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
|
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
|
||||||
@@ -278,7 +279,8 @@ class BaseFirmwareUpdateEntity(
|
|||||||
device=self._current_device,
|
device=self._current_device,
|
||||||
fw_data=fw_data,
|
fw_data=fw_data,
|
||||||
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
|
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
|
||||||
bootloader_reset_methods=self.bootloader_reset_methods,
|
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||||
|
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||||
progress_callback=self._update_progress,
|
progress_callback=self._update_progress,
|
||||||
domain=self._config_entry.domain,
|
domain=self._config_entry.domain,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
|
from collections.abc import AsyncIterator, Callable, Sequence
|
||||||
from contextlib import AsyncExitStack, asynccontextmanager
|
from contextlib import AsyncExitStack, asynccontextmanager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
@@ -309,15 +309,20 @@ async def guess_firmware_info(hass: HomeAssistant, device_path: str) -> Firmware
|
|||||||
|
|
||||||
|
|
||||||
async def probe_silabs_firmware_info(
|
async def probe_silabs_firmware_info(
|
||||||
device: str, *, probe_methods: Iterable[ApplicationType] | None = None
|
device: str,
|
||||||
|
*,
|
||||||
|
bootloader_reset_methods: Sequence[ResetTarget],
|
||||||
|
application_probe_methods: Sequence[tuple[ApplicationType, int]],
|
||||||
) -> FirmwareInfo | None:
|
) -> FirmwareInfo | None:
|
||||||
"""Probe the running firmware on a SiLabs device."""
|
"""Probe the running firmware on a SiLabs device."""
|
||||||
flasher = Flasher(
|
flasher = Flasher(
|
||||||
device=device,
|
device=device,
|
||||||
**(
|
probe_methods=tuple(
|
||||||
{"probe_methods": [m.as_flasher_application_type() for m in probe_methods]}
|
(m.as_flasher_application_type(), baudrate)
|
||||||
if probe_methods
|
for m, baudrate in application_probe_methods
|
||||||
else {}
|
),
|
||||||
|
bootloader_reset=tuple(
|
||||||
|
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -343,11 +348,18 @@ async def probe_silabs_firmware_info(
|
|||||||
|
|
||||||
|
|
||||||
async def probe_silabs_firmware_type(
|
async def probe_silabs_firmware_type(
|
||||||
device: str, *, probe_methods: Iterable[ApplicationType] | None = None
|
device: str,
|
||||||
|
*,
|
||||||
|
bootloader_reset_methods: Sequence[ResetTarget],
|
||||||
|
application_probe_methods: Sequence[tuple[ApplicationType, int]],
|
||||||
) -> ApplicationType | None:
|
) -> ApplicationType | None:
|
||||||
"""Probe the running firmware type on a SiLabs device."""
|
"""Probe the running firmware type on a SiLabs device."""
|
||||||
|
|
||||||
fw_info = await probe_silabs_firmware_info(device, probe_methods=probe_methods)
|
fw_info = await probe_silabs_firmware_info(
|
||||||
|
device,
|
||||||
|
bootloader_reset_methods=bootloader_reset_methods,
|
||||||
|
application_probe_methods=application_probe_methods,
|
||||||
|
)
|
||||||
if fw_info is None:
|
if fw_info is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -359,12 +371,22 @@ async def async_flash_silabs_firmware(
|
|||||||
device: str,
|
device: str,
|
||||||
fw_data: bytes,
|
fw_data: bytes,
|
||||||
expected_installed_firmware_type: ApplicationType,
|
expected_installed_firmware_type: ApplicationType,
|
||||||
bootloader_reset_methods: Sequence[ResetTarget] = (),
|
bootloader_reset_methods: Sequence[ResetTarget],
|
||||||
|
application_probe_methods: Sequence[tuple[ApplicationType, int]],
|
||||||
progress_callback: Callable[[int, int], None] | None = None,
|
progress_callback: Callable[[int, int], None] | None = None,
|
||||||
*,
|
*,
|
||||||
domain: str = DOMAIN,
|
domain: str = DOMAIN,
|
||||||
) -> FirmwareInfo:
|
) -> FirmwareInfo:
|
||||||
"""Flash firmware to the SiLabs device."""
|
"""Flash firmware to the SiLabs device."""
|
||||||
|
if not any(
|
||||||
|
method == expected_installed_firmware_type
|
||||||
|
for method, _ in application_probe_methods
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
f"Expected installed firmware type {expected_installed_firmware_type!r}"
|
||||||
|
f" not in application probe methods {application_probe_methods!r}"
|
||||||
|
)
|
||||||
|
|
||||||
async with async_firmware_update_context(hass, device, domain):
|
async with async_firmware_update_context(hass, device, domain):
|
||||||
firmware_info = await guess_firmware_info(hass, device)
|
firmware_info = await guess_firmware_info(hass, device)
|
||||||
_LOGGER.debug("Identified firmware info: %s", firmware_info)
|
_LOGGER.debug("Identified firmware info: %s", firmware_info)
|
||||||
@@ -373,11 +395,9 @@ async def async_flash_silabs_firmware(
|
|||||||
|
|
||||||
flasher = Flasher(
|
flasher = Flasher(
|
||||||
device=device,
|
device=device,
|
||||||
probe_methods=(
|
probe_methods=tuple(
|
||||||
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
|
(m.as_flasher_application_type(), baudrate)
|
||||||
ApplicationType.EZSP.as_flasher_application_type(),
|
for m, baudrate in application_probe_methods
|
||||||
ApplicationType.SPINEL.as_flasher_application_type(),
|
|
||||||
ApplicationType.CPC.as_flasher_application_type(),
|
|
||||||
),
|
),
|
||||||
bootloader_reset=tuple(
|
bootloader_reset=tuple(
|
||||||
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
||||||
@@ -401,7 +421,13 @@ async def async_flash_silabs_firmware(
|
|||||||
|
|
||||||
probed_firmware_info = await probe_silabs_firmware_info(
|
probed_firmware_info = await probe_silabs_firmware_info(
|
||||||
device,
|
device,
|
||||||
probe_methods=(expected_installed_firmware_type,),
|
bootloader_reset_methods=bootloader_reset_methods,
|
||||||
|
# Only probe for the expected installed firmware type
|
||||||
|
application_probe_methods=[
|
||||||
|
(method, baudrate)
|
||||||
|
for method, baudrate in application_probe_methods
|
||||||
|
if method == expected_installed_firmware_type
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
if probed_firmware_info is None:
|
if probed_firmware_info is None:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from homeassistant.components.homeassistant_hardware.helpers import (
|
|||||||
from homeassistant.components.homeassistant_hardware.util import (
|
from homeassistant.components.homeassistant_hardware.util import (
|
||||||
ApplicationType,
|
ApplicationType,
|
||||||
FirmwareInfo,
|
FirmwareInfo,
|
||||||
|
ResetTarget,
|
||||||
)
|
)
|
||||||
from homeassistant.components.usb import (
|
from homeassistant.components.usb import (
|
||||||
usb_service_info_from_device,
|
usb_service_info_from_device,
|
||||||
@@ -79,6 +80,20 @@ class SkyConnectFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
|||||||
|
|
||||||
context: ConfigFlowContext
|
context: ConfigFlowContext
|
||||||
|
|
||||||
|
ZIGBEE_BAUDRATE = 115200
|
||||||
|
# There is no hardware bootloader trigger
|
||||||
|
BOOTLOADER_RESET_METHODS: list[ResetTarget] = []
|
||||||
|
APPLICATION_PROBE_METHODS = [
|
||||||
|
(ApplicationType.GECKO_BOOTLOADER, 115200),
|
||||||
|
(ApplicationType.EZSP, ZIGBEE_BAUDRATE),
|
||||||
|
(ApplicationType.SPINEL, 460800),
|
||||||
|
# CPC baudrates can be removed once multiprotocol is removed
|
||||||
|
(ApplicationType.CPC, 115200),
|
||||||
|
(ApplicationType.CPC, 230400),
|
||||||
|
(ApplicationType.CPC, 460800),
|
||||||
|
(ApplicationType.ROUTER, 115200),
|
||||||
|
]
|
||||||
|
|
||||||
def _get_translation_placeholders(self) -> dict[str, str]:
|
def _get_translation_placeholders(self) -> dict[str, str]:
|
||||||
"""Shared translation placeholders."""
|
"""Shared translation placeholders."""
|
||||||
placeholders = {
|
placeholders = {
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
"dependencies": ["hardware", "usb", "homeassistant_hardware"],
|
"dependencies": ["hardware", "usb", "homeassistant_hardware"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect",
|
"documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect",
|
||||||
"integration_type": "hardware",
|
"integration_type": "hardware",
|
||||||
|
"loggers": [
|
||||||
|
"bellows",
|
||||||
|
"universal_silabs_flasher",
|
||||||
|
"zigpy.serial",
|
||||||
|
"serial_asyncio_fast"
|
||||||
|
],
|
||||||
"usb": [
|
"usb": [
|
||||||
{
|
{
|
||||||
"description": "*skyconnect v1.0*",
|
"description": "*skyconnect v1.0*",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import HomeAssistantSkyConnectConfigEntry
|
from . import HomeAssistantSkyConnectConfigEntry
|
||||||
|
from .config_flow import SkyConnectFirmwareMixin
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
FIRMWARE,
|
FIRMWARE,
|
||||||
@@ -151,8 +152,8 @@ async def async_setup_entry(
|
|||||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||||
"""SkyConnect firmware update entity."""
|
"""SkyConnect firmware update entity."""
|
||||||
|
|
||||||
# The ZBT-1 does not have a hardware bootloader trigger
|
BOOTLOADER_RESET_METHODS = SkyConnectFirmwareMixin.BOOTLOADER_RESET_METHODS
|
||||||
bootloader_reset_methods = []
|
APPLICATION_PROBE_METHODS = SkyConnectFirmwareMixin.APPLICATION_PROBE_METHODS
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -82,7 +82,18 @@ else:
|
|||||||
class YellowFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
class YellowFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||||
"""Mixin for Home Assistant Yellow firmware methods."""
|
"""Mixin for Home Assistant Yellow firmware methods."""
|
||||||
|
|
||||||
|
ZIGBEE_BAUDRATE = 115200
|
||||||
BOOTLOADER_RESET_METHODS = [ResetTarget.YELLOW]
|
BOOTLOADER_RESET_METHODS = [ResetTarget.YELLOW]
|
||||||
|
APPLICATION_PROBE_METHODS = [
|
||||||
|
(ApplicationType.GECKO_BOOTLOADER, 115200),
|
||||||
|
(ApplicationType.EZSP, ZIGBEE_BAUDRATE),
|
||||||
|
(ApplicationType.SPINEL, 460800),
|
||||||
|
# CPC baudrates can be removed once multiprotocol is removed
|
||||||
|
(ApplicationType.CPC, 115200),
|
||||||
|
(ApplicationType.CPC, 230400),
|
||||||
|
(ApplicationType.CPC, 460800),
|
||||||
|
(ApplicationType.ROUTER, 115200),
|
||||||
|
]
|
||||||
|
|
||||||
async def async_step_install_zigbee_firmware(
|
async def async_step_install_zigbee_firmware(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
@@ -146,7 +157,11 @@ class HomeAssistantYellowConfigFlow(
|
|||||||
assert self._device is not None
|
assert self._device is not None
|
||||||
|
|
||||||
# We do not actually use any portion of `BaseFirmwareConfigFlow` beyond this
|
# We do not actually use any portion of `BaseFirmwareConfigFlow` beyond this
|
||||||
self._probed_firmware_info = await probe_silabs_firmware_info(self._device)
|
self._probed_firmware_info = await probe_silabs_firmware_info(
|
||||||
|
self._device,
|
||||||
|
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||||
|
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||||
|
)
|
||||||
|
|
||||||
# Kick off ZHA hardware discovery automatically if Zigbee firmware is running
|
# Kick off ZHA hardware discovery automatically if Zigbee firmware is running
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -7,5 +7,11 @@
|
|||||||
"dependencies": ["hardware", "homeassistant_hardware"],
|
"dependencies": ["hardware", "homeassistant_hardware"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_yellow",
|
"documentation": "https://www.home-assistant.io/integrations/homeassistant_yellow",
|
||||||
"integration_type": "hardware",
|
"integration_type": "hardware",
|
||||||
|
"loggers": [
|
||||||
|
"bellows",
|
||||||
|
"universal_silabs_flasher",
|
||||||
|
"zigpy.serial",
|
||||||
|
"serial_asyncio_fast"
|
||||||
|
],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from homeassistant.components.homeassistant_hardware.update import (
|
|||||||
from homeassistant.components.homeassistant_hardware.util import (
|
from homeassistant.components.homeassistant_hardware.util import (
|
||||||
ApplicationType,
|
ApplicationType,
|
||||||
FirmwareInfo,
|
FirmwareInfo,
|
||||||
ResetTarget,
|
|
||||||
)
|
)
|
||||||
from homeassistant.components.update import UpdateDeviceClass
|
from homeassistant.components.update import UpdateDeviceClass
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
@@ -24,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import HomeAssistantYellowConfigEntry
|
from . import HomeAssistantYellowConfigEntry
|
||||||
|
from .config_flow import YellowFirmwareMixin
|
||||||
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, MANUFACTURER, MODEL, RADIO_DEVICE
|
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, MANUFACTURER, MODEL, RADIO_DEVICE
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -150,7 +150,8 @@ async def async_setup_entry(
|
|||||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||||
"""Yellow firmware update entity."""
|
"""Yellow firmware update entity."""
|
||||||
|
|
||||||
bootloader_reset_methods = [ResetTarget.YELLOW] # Triggers a GPIO reset
|
BOOTLOADER_RESET_METHODS = YellowFirmwareMixin.BOOTLOADER_RESET_METHODS
|
||||||
|
APPLICATION_PROBE_METHODS = YellowFirmwareMixin.APPLICATION_PROBE_METHODS
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -121,12 +121,15 @@ class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]):
|
|||||||
"""Initialize AutomowerEntity."""
|
"""Initialize AutomowerEntity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.mower_id = mower_id
|
self.mower_id = mower_id
|
||||||
parts = self.mower_attributes.system.model.split(maxsplit=2)
|
model_witout_manufacturer = self.mower_attributes.system.model.removeprefix(
|
||||||
|
"Husqvarna "
|
||||||
|
).removeprefix("HUSQVARNA ")
|
||||||
|
parts = model_witout_manufacturer.split(maxsplit=1)
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, mower_id)},
|
identifiers={(DOMAIN, mower_id)},
|
||||||
manufacturer=parts[0],
|
manufacturer="Husqvarna",
|
||||||
model=parts[1],
|
model=parts[0].capitalize().removesuffix("®"),
|
||||||
model_id=parts[2],
|
model_id=parts[1],
|
||||||
name=self.mower_attributes.system.name,
|
name=self.mower_attributes.system.name,
|
||||||
serial_number=self.mower_attributes.system.serial_number,
|
serial_number=self.mower_attributes.system.serial_number,
|
||||||
suggested_area="Garden",
|
suggested_area="Garden",
|
||||||
|
|||||||
@@ -37,5 +37,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pylamarzocco"],
|
"loggers": ["pylamarzocco"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pylamarzocco==2.1.2"]
|
"requirements": ["pylamarzocco==2.1.3"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity):
|
|||||||
await self.coordinator.device.update_firmware()
|
await self.coordinator.device.update_firmware()
|
||||||
while (
|
while (
|
||||||
update_progress := await self.coordinator.device.get_firmware()
|
update_progress := await self.coordinator.device.get_firmware()
|
||||||
).command_status is UpdateStatus.IN_PROGRESS:
|
).command_status is not UpdateStatus.UPDATED:
|
||||||
if counter >= MAX_UPDATE_WAIT:
|
if counter >= MAX_UPDATE_WAIT:
|
||||||
_raise_timeout_error()
|
_raise_timeout_error()
|
||||||
self._attr_update_percentage = update_progress.progress_percentage
|
self._attr_update_percentage = update_progress.progress_percentage
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["ical"],
|
"loggers": ["ical"],
|
||||||
"requirements": ["ical==11.0.0"]
|
"requirements": ["ical==11.1.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["ical==11.0.0"]
|
"requirements": ["ical==11.1.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ class LocalTodoListEntity(TodoListEntity):
|
|||||||
),
|
),
|
||||||
due=due,
|
due=due,
|
||||||
description=item.description,
|
description=item.description,
|
||||||
|
completed=item.completed,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self._attr_todo_items = todo_items
|
self._attr_todo_items = todo_items
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["lunatone-rest-api-client==0.5.3"]
|
"requirements": ["lunatone-rest-api-client==0.5.7"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,7 @@
|
|||||||
"name": "Altitude above sea level"
|
"name": "Altitude above sea level"
|
||||||
},
|
},
|
||||||
"auto_relock_timer": {
|
"auto_relock_timer": {
|
||||||
"name": "Autorelock time"
|
"name": "Auto-relock time"
|
||||||
},
|
},
|
||||||
"cook_time": {
|
"cook_time": {
|
||||||
"name": "Cooking time"
|
"name": "Cooking time"
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class MeteoLtWeatherEntity(CoordinatorEntity[MeteoLtUpdateCoordinator], WeatherE
|
|||||||
forecasts_by_date[date].append(timestamp)
|
forecasts_by_date[date].append(timestamp)
|
||||||
|
|
||||||
daily_forecasts = []
|
daily_forecasts = []
|
||||||
for date in sorted(forecasts_by_date.keys())[:5]:
|
for date in sorted(forecasts_by_date.keys()):
|
||||||
day_forecasts = forecasts_by_date[date]
|
day_forecasts = forecasts_by_date[date]
|
||||||
if not day_forecasts:
|
if not day_forecasts:
|
||||||
continue
|
continue
|
||||||
@@ -186,5 +186,5 @@ class MeteoLtWeatherEntity(CoordinatorEntity[MeteoLtUpdateCoordinator], WeatherE
|
|||||||
return None
|
return None
|
||||||
return [
|
return [
|
||||||
self._convert_forecast_data(forecast_data)
|
self._convert_forecast_data(forecast_data)
|
||||||
for forecast_data in self.coordinator.data.forecast_timestamps[:24]
|
for forecast_data in self.coordinator.data.forecast_timestamps
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
|||||||
import logging
|
import logging
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
import aiohttp
|
from aiohttp import ClientResponseError
|
||||||
|
|
||||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -153,11 +153,12 @@ class MieleButton(MieleEntity, ButtonEntity):
|
|||||||
self._device_id,
|
self._device_id,
|
||||||
{PROCESS_ACTION: self.entity_description.press_data},
|
{PROCESS_ACTION: self.entity_description.press_data},
|
||||||
)
|
)
|
||||||
except aiohttp.ClientResponseError as ex:
|
except ClientResponseError as err:
|
||||||
|
_LOGGER.debug("Error setting button state for %s: %s", self.entity_id, err)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="set_state_error",
|
translation_key="set_state_error",
|
||||||
translation_placeholders={
|
translation_placeholders={
|
||||||
"entity": self.entity_id,
|
"entity": self.entity_id,
|
||||||
},
|
},
|
||||||
) from ex
|
) from err
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Final, cast
|
from typing import Any, Final, cast
|
||||||
|
|
||||||
import aiohttp
|
from aiohttp import ClientResponseError
|
||||||
from pymiele import MieleDevice, MieleTemperature
|
from pymiele import MieleDevice, MieleTemperature
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
@@ -250,7 +250,8 @@ class MieleClimate(MieleEntity, ClimateEntity):
|
|||||||
cast(float, kwargs.get(ATTR_TEMPERATURE)),
|
cast(float, kwargs.get(ATTR_TEMPERATURE)),
|
||||||
self.entity_description.zone,
|
self.entity_description.zone,
|
||||||
)
|
)
|
||||||
except aiohttp.ClientError as err:
|
except ClientResponseError as err:
|
||||||
|
_LOGGER.debug("Error setting climate state for %s: %s", self.entity_id, err)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="set_state_error",
|
translation_key="set_state_error",
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]):
|
|||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Error fetching actions for device %s: Status: %s, Message: %s",
|
"Error fetching actions for device %s: Status: %s, Message: %s",
|
||||||
device_id,
|
device_id,
|
||||||
err.status,
|
str(err.status),
|
||||||
err.message,
|
err.message,
|
||||||
)
|
)
|
||||||
actions_json = {}
|
actions_json = {}
|
||||||
|
|||||||
@@ -142,14 +142,15 @@ class MieleFan(MieleEntity, FanEntity):
|
|||||||
await self.api.send_action(
|
await self.api.send_action(
|
||||||
self._device_id, {VENTILATION_STEP: ventilation_step}
|
self._device_id, {VENTILATION_STEP: ventilation_step}
|
||||||
)
|
)
|
||||||
except ClientResponseError as ex:
|
except ClientResponseError as err:
|
||||||
|
_LOGGER.debug("Error setting fan state for %s: %s", self.entity_id, err)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="set_state_error",
|
translation_key="set_state_error",
|
||||||
translation_placeholders={
|
translation_placeholders={
|
||||||
"entity": self.entity_id,
|
"entity": self.entity_id,
|
||||||
},
|
},
|
||||||
) from ex
|
) from err
|
||||||
self.device.state_ventilation_step = ventilation_step
|
self.device.state_ventilation_step = ventilation_step
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@@ -171,6 +172,7 @@ class MieleFan(MieleEntity, FanEntity):
|
|||||||
translation_key="set_state_error",
|
translation_key="set_state_error",
|
||||||
translation_placeholders={
|
translation_placeholders={
|
||||||
"entity": self.entity_id,
|
"entity": self.entity_id,
|
||||||
|
"err_status": str(ex.status),
|
||||||
},
|
},
|
||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
@@ -188,6 +190,7 @@ class MieleFan(MieleEntity, FanEntity):
|
|||||||
translation_key="set_state_error",
|
translation_key="set_state_error",
|
||||||
translation_placeholders={
|
translation_placeholders={
|
||||||
"entity": self.entity_id,
|
"entity": self.entity_id,
|
||||||
|
"err_status": str(ex.status),
|
||||||
},
|
},
|
||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
import aiohttp
|
from aiohttp import ClientResponseError
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ColorMode,
|
ColorMode,
|
||||||
@@ -131,7 +131,8 @@ class MieleLight(MieleEntity, LightEntity):
|
|||||||
await self.api.send_action(
|
await self.api.send_action(
|
||||||
self._device_id, {self.entity_description.light_type: mode}
|
self._device_id, {self.entity_description.light_type: mode}
|
||||||
)
|
)
|
||||||
except aiohttp.ClientError as err:
|
except ClientResponseError as err:
|
||||||
|
_LOGGER.debug("Error setting light state for %s: %s", self.entity_id, err)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="set_state_error",
|
translation_key="set_state_error",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from datetime import timedelta
|
|||||||
import logging
|
import logging
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
import aiohttp
|
from aiohttp import ClientResponseError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import ATTR_DEVICE_ID, ATTR_TEMPERATURE
|
from homeassistant.const import ATTR_DEVICE_ID, ATTR_TEMPERATURE
|
||||||
@@ -107,7 +107,7 @@ async def set_program(call: ServiceCall) -> None:
|
|||||||
data = {"programId": call.data[ATTR_PROGRAM_ID]}
|
data = {"programId": call.data[ATTR_PROGRAM_ID]}
|
||||||
try:
|
try:
|
||||||
await api.set_program(serial_number, data)
|
await api.set_program(serial_number, data)
|
||||||
except aiohttp.ClientResponseError as ex:
|
except ClientResponseError as ex:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="set_program_error",
|
translation_key="set_program_error",
|
||||||
@@ -137,7 +137,7 @@ async def set_program_oven(call: ServiceCall) -> None:
|
|||||||
data["temperature"] = call.data[ATTR_TEMPERATURE]
|
data["temperature"] = call.data[ATTR_TEMPERATURE]
|
||||||
try:
|
try:
|
||||||
await api.set_program(serial_number, data)
|
await api.set_program(serial_number, data)
|
||||||
except aiohttp.ClientResponseError as ex:
|
except ClientResponseError as ex:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="set_program_oven_error",
|
translation_key="set_program_oven_error",
|
||||||
@@ -157,7 +157,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
programs = await api.get_programs(serial_number)
|
programs = await api.get_programs(serial_number)
|
||||||
except aiohttp.ClientResponseError as ex:
|
except ClientResponseError as ex:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="get_programs_error",
|
translation_key="get_programs_error",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Final, cast
|
from typing import Any, Final, cast
|
||||||
|
|
||||||
import aiohttp
|
from aiohttp import ClientResponseError
|
||||||
from pymiele import MieleDevice
|
from pymiele import MieleDevice
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
@@ -165,7 +165,8 @@ class MieleSwitch(MieleEntity, SwitchEntity):
|
|||||||
"""Set switch to mode."""
|
"""Set switch to mode."""
|
||||||
try:
|
try:
|
||||||
await self.api.send_action(self._device_id, mode)
|
await self.api.send_action(self._device_id, mode)
|
||||||
except aiohttp.ClientError as err:
|
except ClientResponseError as err:
|
||||||
|
_LOGGER.debug("Error setting switch state for %s: %s", self.entity_id, err)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="set_state_error",
|
translation_key="set_state_error",
|
||||||
@@ -197,7 +198,8 @@ class MielePowerSwitch(MieleSwitch):
|
|||||||
"""Set switch to mode."""
|
"""Set switch to mode."""
|
||||||
try:
|
try:
|
||||||
await self.api.send_action(self._device_id, mode)
|
await self.api.send_action(self._device_id, mode)
|
||||||
except aiohttp.ClientError as err:
|
except ClientResponseError as err:
|
||||||
|
_LOGGER.debug("Error setting switch state for %s: %s", self.entity_id, err)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="set_state_error",
|
translation_key="set_state_error",
|
||||||
|
|||||||
@@ -189,14 +189,15 @@ class MieleVacuum(MieleEntity, StateVacuumEntity):
|
|||||||
"""Send action to the device."""
|
"""Send action to the device."""
|
||||||
try:
|
try:
|
||||||
await self.api.send_action(device_id, action)
|
await self.api.send_action(device_id, action)
|
||||||
except ClientResponseError as ex:
|
except ClientResponseError as err:
|
||||||
|
_LOGGER.debug("Error setting vacuum state for %s: %s", self.entity_id, err)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="set_state_error",
|
translation_key="set_state_error",
|
||||||
translation_placeholders={
|
translation_placeholders={
|
||||||
"entity": self.entity_id,
|
"entity": self.entity_id,
|
||||||
},
|
},
|
||||||
) from ex
|
) from err
|
||||||
|
|
||||||
async def async_clean_spot(self, **kwargs: Any) -> None:
|
async def async_clean_spot(self, **kwargs: Any) -> None:
|
||||||
"""Clean spot."""
|
"""Clean spot."""
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/mill",
|
"documentation": "https://www.home-assistant.io/integrations/mill",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["mill", "mill_local"],
|
"loggers": ["mill", "mill_local"],
|
||||||
"requirements": ["millheater==0.14.0", "mill-local==0.3.0"]
|
"requirements": ["millheater==0.14.1", "mill-local==0.3.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,10 +61,12 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
async_add_entities([MobileAppBinarySensor(data, config_entry)])
|
async_add_entities([MobileAppBinarySensor(data, config_entry)])
|
||||||
|
|
||||||
async_dispatcher_connect(
|
config_entry.async_on_unload(
|
||||||
hass,
|
async_dispatcher_connect(
|
||||||
f"{DOMAIN}_{ENTITY_TYPE}_register",
|
hass,
|
||||||
handle_sensor_registration,
|
f"{DOMAIN}_{ENTITY_TYPE}_register",
|
||||||
|
handle_sensor_registration,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,10 +72,12 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
async_add_entities([MobileAppSensor(data, config_entry)])
|
async_add_entities([MobileAppSensor(data, config_entry)])
|
||||||
|
|
||||||
async_dispatcher_connect(
|
config_entry.async_on_unload(
|
||||||
hass,
|
async_dispatcher_connect(
|
||||||
f"{DOMAIN}_{ENTITY_TYPE}_register",
|
hass,
|
||||||
handle_sensor_registration,
|
f"{DOMAIN}_{ENTITY_TYPE}_register",
|
||||||
|
handle_sensor_registration,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ class ModbusRegisterSensor(ModbusStructEntity, RestoreSensor, SensorEntity):
|
|||||||
self._attr_device_class = entry.get(CONF_DEVICE_CLASS)
|
self._attr_device_class = entry.get(CONF_DEVICE_CLASS)
|
||||||
if self._precision > 0 or self._scale != int(self._scale):
|
if self._precision > 0 or self._scale != int(self._scale):
|
||||||
self._value_is_int = False
|
self._value_is_int = False
|
||||||
|
if self._precision > 0 and self._data_type not in ["string", "custom"]:
|
||||||
|
self._attr_suggested_display_precision = self._precision
|
||||||
|
|
||||||
async def async_setup_slaves(
|
async def async_setup_slaves(
|
||||||
self, hass: HomeAssistant, slave_count: int, entry: dict[str, Any]
|
self, hass: HomeAssistant, slave_count: int, entry: dict[str, Any]
|
||||||
|
|||||||
@@ -1312,6 +1312,7 @@
|
|||||||
"speed": "[%key:component::sensor::entity_component::speed::name%]",
|
"speed": "[%key:component::sensor::entity_component::speed::name%]",
|
||||||
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
|
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
|
||||||
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
|
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
|
||||||
|
"temperature_delta": "[%key:component::sensor::entity_component::temperature_delta::name%]",
|
||||||
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
|
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
|
||||||
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::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_parts::name%]",
|
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]",
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/mvglive",
|
"documentation": "https://www.home-assistant.io/integrations/mvglive",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["MVG"],
|
"loggers": ["MVG"],
|
||||||
|
"quality_scale": "legacy",
|
||||||
"requirements": ["mvg==1.4.0"]
|
"requirements": ["mvg==1.4.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
"message": "Cannot read {filename}, no access to path; `allowlist_external_dirs` may need to be adjusted in `configuration.yaml`"
|
"message": "Cannot read {filename}, no access to path; `allowlist_external_dirs` may need to be adjusted in `configuration.yaml`"
|
||||||
},
|
},
|
||||||
"oauth2_implementation_unavailable": {
|
"oauth2_implementation_unavailable": {
|
||||||
"message": "OAuth2 implementation unavailable, will retry"
|
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||||
},
|
},
|
||||||
"update_failed": {
|
"update_failed": {
|
||||||
"message": "Failed to update drive state"
|
"message": "Failed to update drive state"
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["openai==2.2.0", "python-open-router==0.3.2"]
|
"requirements": ["openai==2.8.0", "python-open-router==0.3.3"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,6 +338,13 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
options.pop(CONF_CODE_INTERPRETER)
|
options.pop(CONF_CODE_INTERPRETER)
|
||||||
|
|
||||||
if model.startswith(("o", "gpt-5")) and not model.startswith("gpt-5-pro"):
|
if model.startswith(("o", "gpt-5")) and not model.startswith("gpt-5-pro"):
|
||||||
|
if model.startswith("gpt-5.1"):
|
||||||
|
reasoning_options = ["none", "low", "medium", "high"]
|
||||||
|
elif model.startswith("gpt-5"):
|
||||||
|
reasoning_options = ["minimal", "low", "medium", "high"]
|
||||||
|
else:
|
||||||
|
reasoning_options = ["low", "medium", "high"]
|
||||||
|
|
||||||
step_schema.update(
|
step_schema.update(
|
||||||
{
|
{
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
@@ -345,9 +352,7 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
default=RECOMMENDED_REASONING_EFFORT,
|
default=RECOMMENDED_REASONING_EFFORT,
|
||||||
): SelectSelector(
|
): SelectSelector(
|
||||||
SelectSelectorConfig(
|
SelectSelectorConfig(
|
||||||
options=["low", "medium", "high"]
|
options=reasoning_options,
|
||||||
if model.startswith("o")
|
|
||||||
else ["minimal", "low", "medium", "high"],
|
|
||||||
translation_key=CONF_REASONING_EFFORT,
|
translation_key=CONF_REASONING_EFFORT,
|
||||||
mode=SelectSelectorMode.DROPDOWN,
|
mode=SelectSelectorMode.DROPDOWN,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -510,6 +510,9 @@ class OpenAIBaseLLMEntity(Entity):
|
|||||||
"verbosity": options.get(CONF_VERBOSITY, RECOMMENDED_VERBOSITY)
|
"verbosity": options.get(CONF_VERBOSITY, RECOMMENDED_VERBOSITY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if model_args["model"].startswith("gpt-5.1"):
|
||||||
|
model_args["prompt_cache_retention"] = "24h"
|
||||||
|
|
||||||
tools: list[ToolParam] = []
|
tools: list[ToolParam] = []
|
||||||
if chat_log.llm_api:
|
if chat_log.llm_api:
|
||||||
tools = [
|
tools = [
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/openai_conversation",
|
"documentation": "https://www.home-assistant.io/integrations/openai_conversation",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["openai==2.2.0"]
|
"requirements": ["openai==2.8.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,7 +146,8 @@
|
|||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"medium": "[%key:common::state::medium%]",
|
"medium": "[%key:common::state::medium%]",
|
||||||
"minimal": "Minimal"
|
"minimal": "Minimal",
|
||||||
|
"none": "None"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search_context_size": {
|
"search_context_size": {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
"oauth2_implementation_unavailable": {
|
"oauth2_implementation_unavailable": {
|
||||||
"message": "OAuth2 implementation unavailable, will retry"
|
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,17 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.const import UnitOfTemperature, UnitOfVolumeFlowRate
|
||||||
|
|
||||||
DOMAIN = "pooldose"
|
DOMAIN = "pooldose"
|
||||||
MANUFACTURER = "SEKO"
|
MANUFACTURER = "SEKO"
|
||||||
|
|
||||||
|
# Mapping of device units to Home Assistant units
|
||||||
|
UNIT_MAPPING: dict[str, str] = {
|
||||||
|
# Temperature units
|
||||||
|
"°C": UnitOfTemperature.CELSIUS,
|
||||||
|
"°F": UnitOfTemperature.FAHRENHEIT,
|
||||||
|
# Volume flow rate units
|
||||||
|
"m3/h": UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
||||||
|
"L/s": UnitOfVolumeFlowRate.LITERS_PER_SECOND,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
{
|
{
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
"cl": {
|
||||||
|
"default": "mdi:pool"
|
||||||
|
},
|
||||||
|
"cl_type_dosing": {
|
||||||
|
"default": "mdi:flask"
|
||||||
|
},
|
||||||
|
"flow_rate": {
|
||||||
|
"default": "mdi:pipe-valve"
|
||||||
|
},
|
||||||
"ofa_orp_time": {
|
"ofa_orp_time": {
|
||||||
"default": "mdi:clock"
|
"default": "mdi:clock"
|
||||||
},
|
},
|
||||||
@@ -22,6 +31,9 @@
|
|||||||
"orp_type_dosing": {
|
"orp_type_dosing": {
|
||||||
"default": "mdi:flask"
|
"default": "mdi:flask"
|
||||||
},
|
},
|
||||||
|
"peristaltic_cl_dosing": {
|
||||||
|
"default": "mdi:pump"
|
||||||
|
},
|
||||||
"peristaltic_orp_dosing": {
|
"peristaltic_orp_dosing": {
|
||||||
"default": "mdi:pump"
|
"default": "mdi:pump"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -10,36 +11,61 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.const import EntityCategory, UnitOfElectricPotential, UnitOfTime
|
from homeassistant.const import (
|
||||||
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
EntityCategory,
|
||||||
|
UnitOfElectricPotential,
|
||||||
|
UnitOfTime,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import PooldoseConfigEntry
|
from . import PooldoseConfigEntry
|
||||||
|
from .const import UNIT_MAPPING
|
||||||
from .entity import PooldoseEntity
|
from .entity import PooldoseEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
|
||||||
SensorEntityDescription(
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class PooldoseSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes PoolDose sensor entity."""
|
||||||
|
|
||||||
|
use_dynamic_unit: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
SENSOR_DESCRIPTIONS: tuple[PooldoseSensorEntityDescription, ...] = (
|
||||||
|
PooldoseSensorEntityDescription(
|
||||||
key="temperature",
|
key="temperature",
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
# Unit dynamically determined via API
|
use_dynamic_unit=True,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(key="ph", device_class=SensorDeviceClass.PH),
|
PooldoseSensorEntityDescription(key="ph", device_class=SensorDeviceClass.PH),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
key="orp",
|
key="orp",
|
||||||
translation_key="orp",
|
translation_key="orp",
|
||||||
device_class=SensorDeviceClass.VOLTAGE,
|
device_class=SensorDeviceClass.VOLTAGE,
|
||||||
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
|
key="cl",
|
||||||
|
translation_key="cl",
|
||||||
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
),
|
||||||
|
PooldoseSensorEntityDescription(
|
||||||
|
key="flow_rate",
|
||||||
|
translation_key="flow_rate",
|
||||||
|
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||||
|
use_dynamic_unit=True,
|
||||||
|
),
|
||||||
|
PooldoseSensorEntityDescription(
|
||||||
key="ph_type_dosing",
|
key="ph_type_dosing",
|
||||||
translation_key="ph_type_dosing",
|
translation_key="ph_type_dosing",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
options=["alcalyne", "acid"],
|
options=["alcalyne", "acid"],
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
key="peristaltic_ph_dosing",
|
key="peristaltic_ph_dosing",
|
||||||
translation_key="peristaltic_ph_dosing",
|
translation_key="peristaltic_ph_dosing",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@@ -47,7 +73,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
options=["proportional", "on_off", "timed"],
|
options=["proportional", "on_off", "timed"],
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
key="ofa_ph_time",
|
key="ofa_ph_time",
|
||||||
translation_key="ofa_ph_time",
|
translation_key="ofa_ph_time",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@@ -55,7 +81,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
|||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
key="orp_type_dosing",
|
key="orp_type_dosing",
|
||||||
translation_key="orp_type_dosing",
|
translation_key="orp_type_dosing",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@@ -63,7 +89,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
options=["low", "high"],
|
options=["low", "high"],
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
key="peristaltic_orp_dosing",
|
key="peristaltic_orp_dosing",
|
||||||
translation_key="peristaltic_orp_dosing",
|
translation_key="peristaltic_orp_dosing",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@@ -71,7 +97,23 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
options=["off", "proportional", "on_off", "timed"],
|
options=["off", "proportional", "on_off", "timed"],
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
|
key="cl_type_dosing",
|
||||||
|
translation_key="cl_type_dosing",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=["low", "high"],
|
||||||
|
),
|
||||||
|
PooldoseSensorEntityDescription(
|
||||||
|
key="peristaltic_cl_dosing",
|
||||||
|
translation_key="peristaltic_cl_dosing",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=["off", "proportional", "on_off", "timed"],
|
||||||
|
),
|
||||||
|
PooldoseSensorEntityDescription(
|
||||||
key="ofa_orp_time",
|
key="ofa_orp_time",
|
||||||
translation_key="ofa_orp_time",
|
translation_key="ofa_orp_time",
|
||||||
device_class=SensorDeviceClass.DURATION,
|
device_class=SensorDeviceClass.DURATION,
|
||||||
@@ -79,7 +121,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
|||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
key="ph_calibration_type",
|
key="ph_calibration_type",
|
||||||
translation_key="ph_calibration_type",
|
translation_key="ph_calibration_type",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@@ -87,7 +129,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
options=["off", "reference", "1_point", "2_points"],
|
options=["off", "reference", "1_point", "2_points"],
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
key="ph_calibration_offset",
|
key="ph_calibration_offset",
|
||||||
translation_key="ph_calibration_offset",
|
translation_key="ph_calibration_offset",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@@ -96,7 +138,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
|||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
key="ph_calibration_slope",
|
key="ph_calibration_slope",
|
||||||
translation_key="ph_calibration_slope",
|
translation_key="ph_calibration_slope",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@@ -105,7 +147,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
|||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
key="orp_calibration_type",
|
key="orp_calibration_type",
|
||||||
translation_key="orp_calibration_type",
|
translation_key="orp_calibration_type",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@@ -113,7 +155,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
options=["off", "reference", "1_point"],
|
options=["off", "reference", "1_point"],
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
key="orp_calibration_offset",
|
key="orp_calibration_offset",
|
||||||
translation_key="orp_calibration_offset",
|
translation_key="orp_calibration_offset",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@@ -122,7 +164,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
|||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
PooldoseSensorEntityDescription(
|
||||||
key="orp_calibration_slope",
|
key="orp_calibration_slope",
|
||||||
translation_key="orp_calibration_slope",
|
translation_key="orp_calibration_slope",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@@ -163,6 +205,8 @@ async def async_setup_entry(
|
|||||||
class PooldoseSensor(PooldoseEntity, SensorEntity):
|
class PooldoseSensor(PooldoseEntity, SensorEntity):
|
||||||
"""Sensor entity for the Seko PoolDose Python API."""
|
"""Sensor entity for the Seko PoolDose Python API."""
|
||||||
|
|
||||||
|
entity_description: PooldoseSensorEntityDescription
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> float | int | str | None:
|
def native_value(self) -> float | int | str | None:
|
||||||
"""Return the current value of the sensor."""
|
"""Return the current value of the sensor."""
|
||||||
@@ -175,9 +219,12 @@ class PooldoseSensor(PooldoseEntity, SensorEntity):
|
|||||||
def native_unit_of_measurement(self) -> str | None:
|
def native_unit_of_measurement(self) -> str | None:
|
||||||
"""Return the unit of measurement."""
|
"""Return the unit of measurement."""
|
||||||
if (
|
if (
|
||||||
self.entity_description.key == "temperature"
|
self.entity_description.use_dynamic_unit
|
||||||
and (data := self.get_data()) is not None
|
and (data := self.get_data()) is not None
|
||||||
|
and (device_unit := data.get("unit"))
|
||||||
):
|
):
|
||||||
return data["unit"] # °C or °F
|
# Map device unit to Home Assistant unit, return None if unknown
|
||||||
|
return UNIT_MAPPING.get(device_unit)
|
||||||
|
|
||||||
|
# Fall back to static unit from entity description
|
||||||
return super().native_unit_of_measurement
|
return super().native_unit_of_measurement
|
||||||
|
|||||||
@@ -34,6 +34,19 @@
|
|||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
"cl": {
|
||||||
|
"name": "Chlorine"
|
||||||
|
},
|
||||||
|
"cl_type_dosing": {
|
||||||
|
"name": "Chlorine dosing type",
|
||||||
|
"state": {
|
||||||
|
"high": "[%key:common::state::high%]",
|
||||||
|
"low": "[%key:common::state::low%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flow_rate": {
|
||||||
|
"name": "Flow rate"
|
||||||
|
},
|
||||||
"ofa_orp_time": {
|
"ofa_orp_time": {
|
||||||
"name": "ORP overfeed alert time"
|
"name": "ORP overfeed alert time"
|
||||||
},
|
},
|
||||||
@@ -64,6 +77,15 @@
|
|||||||
"low": "[%key:common::state::low%]"
|
"low": "[%key:common::state::low%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"peristaltic_cl_dosing": {
|
||||||
|
"name": "Chlorine peristaltic dosing",
|
||||||
|
"state": {
|
||||||
|
"off": "[%key:common::state::off%]",
|
||||||
|
"on_off": "[%key:component::pooldose::entity::sensor::peristaltic_ph_dosing::state::on_off%]",
|
||||||
|
"proportional": "[%key:component::pooldose::entity::sensor::peristaltic_ph_dosing::state::proportional%]",
|
||||||
|
"timed": "[%key:component::pooldose::entity::sensor::peristaltic_ph_dosing::state::timed%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
"peristaltic_orp_dosing": {
|
"peristaltic_orp_dosing": {
|
||||||
"name": "ORP peristaltic dosing",
|
"name": "ORP peristaltic dosing",
|
||||||
"state": {
|
"state": {
|
||||||
|
|||||||
@@ -128,6 +128,7 @@
|
|||||||
"speed": "[%key:component::sensor::entity_component::speed::name%]",
|
"speed": "[%key:component::sensor::entity_component::speed::name%]",
|
||||||
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
|
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
|
||||||
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
|
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
|
||||||
|
"temperature_delta": "[%key:component::sensor::entity_component::temperature_delta::name%]",
|
||||||
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
|
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
|
||||||
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::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_parts::name%]",
|
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from sqlalchemy.orm import DeclarativeBase
|
|||||||
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||||
|
|
||||||
from ..const import SupportedDialect
|
from ..const import SupportedDialect
|
||||||
from ..db_schema import DOUBLE_PRECISION_TYPE_SQL, DOUBLE_TYPE
|
from ..db_schema import DOUBLE_PRECISION_TYPE_SQL, DOUBLE_TYPE, MYSQL_COLLATE
|
||||||
from ..util import session_scope
|
from ..util import session_scope
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -105,12 +105,13 @@ def _validate_table_schema_has_correct_collation(
|
|||||||
or dialect_kwargs.get("mariadb_collate")
|
or dialect_kwargs.get("mariadb_collate")
|
||||||
or connection.dialect._fetch_setting(connection, "collation_server") # type: ignore[attr-defined] # noqa: SLF001
|
or connection.dialect._fetch_setting(connection, "collation_server") # type: ignore[attr-defined] # noqa: SLF001
|
||||||
)
|
)
|
||||||
if collate and collate != "utf8mb4_unicode_ci":
|
if collate and collate != MYSQL_COLLATE:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Database %s collation is not utf8mb4_unicode_ci",
|
"Database %s collation is not %s",
|
||||||
table,
|
table,
|
||||||
|
MYSQL_COLLATE,
|
||||||
)
|
)
|
||||||
schema_errors.add(f"{table}.utf8mb4_unicode_ci")
|
schema_errors.add(f"{table}.{MYSQL_COLLATE}")
|
||||||
return schema_errors
|
return schema_errors
|
||||||
|
|
||||||
|
|
||||||
@@ -240,7 +241,7 @@ def correct_db_schema_utf8(
|
|||||||
table_name = table_object.__tablename__
|
table_name = table_object.__tablename__
|
||||||
if (
|
if (
|
||||||
f"{table_name}.4-byte UTF-8" in schema_errors
|
f"{table_name}.4-byte UTF-8" in schema_errors
|
||||||
or f"{table_name}.utf8mb4_unicode_ci" in schema_errors
|
or f"{table_name}.{MYSQL_COLLATE}" in schema_errors
|
||||||
):
|
):
|
||||||
from ..migration import ( # noqa: PLC0415
|
from ..migration import ( # noqa: PLC0415
|
||||||
_correct_table_character_set_and_collation,
|
_correct_table_character_set_and_collation,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class LegacyBase(DeclarativeBase):
|
|||||||
"""Base class for tables, used for schema migration."""
|
"""Base class for tables, used for schema migration."""
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_VERSION = 52
|
SCHEMA_VERSION = 53
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ LEGACY_STATES_ENTITY_ID_LAST_UPDATED_TS_INDEX = "ix_states_entity_id_last_update
|
|||||||
LEGACY_MAX_LENGTH_EVENT_CONTEXT_ID: Final = 36
|
LEGACY_MAX_LENGTH_EVENT_CONTEXT_ID: Final = 36
|
||||||
CONTEXT_ID_BIN_MAX_LENGTH = 16
|
CONTEXT_ID_BIN_MAX_LENGTH = 16
|
||||||
|
|
||||||
MYSQL_COLLATE = "utf8mb4_unicode_ci"
|
MYSQL_COLLATE = "utf8mb4_bin"
|
||||||
MYSQL_DEFAULT_CHARSET = "utf8mb4"
|
MYSQL_DEFAULT_CHARSET = "utf8mb4"
|
||||||
MYSQL_ENGINE = "InnoDB"
|
MYSQL_ENGINE = "InnoDB"
|
||||||
|
|
||||||
|
|||||||
@@ -1361,7 +1361,7 @@ class _SchemaVersion20Migrator(_SchemaVersionMigrator, target_version=20):
|
|||||||
class _SchemaVersion21Migrator(_SchemaVersionMigrator, target_version=21):
|
class _SchemaVersion21Migrator(_SchemaVersionMigrator, target_version=21):
|
||||||
def _apply_update(self) -> None:
|
def _apply_update(self) -> None:
|
||||||
"""Version specific update method."""
|
"""Version specific update method."""
|
||||||
# Try to change the character set of the statistic_meta table
|
# Try to change the character set of events, states and statistics_meta tables
|
||||||
if self.engine.dialect.name == SupportedDialect.MYSQL:
|
if self.engine.dialect.name == SupportedDialect.MYSQL:
|
||||||
for table in ("events", "states", "statistics_meta"):
|
for table in ("events", "states", "statistics_meta"):
|
||||||
_correct_table_character_set_and_collation(table, self.session_maker)
|
_correct_table_character_set_and_collation(table, self.session_maker)
|
||||||
@@ -2125,6 +2125,23 @@ class _SchemaVersion52Migrator(_SchemaVersionMigrator, target_version=52):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _SchemaVersion53Migrator(_SchemaVersionMigrator, target_version=53):
|
||||||
|
def _apply_update(self) -> None:
|
||||||
|
"""Version specific update method."""
|
||||||
|
# Try to change the character set of events, states and statistics_meta tables
|
||||||
|
if self.engine.dialect.name == SupportedDialect.MYSQL:
|
||||||
|
for table in (
|
||||||
|
"events",
|
||||||
|
"event_data",
|
||||||
|
"states",
|
||||||
|
"state_attributes",
|
||||||
|
"statistics",
|
||||||
|
"statistics_meta",
|
||||||
|
"statistics_short_term",
|
||||||
|
):
|
||||||
|
_correct_table_character_set_and_collation(table, self.session_maker)
|
||||||
|
|
||||||
|
|
||||||
def _migrate_statistics_columns_to_timestamp_removing_duplicates(
|
def _migrate_statistics_columns_to_timestamp_removing_duplicates(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
instance: Recorder,
|
instance: Recorder,
|
||||||
@@ -2167,8 +2184,10 @@ def _correct_table_character_set_and_collation(
|
|||||||
"""Correct issues detected by validate_db_schema."""
|
"""Correct issues detected by validate_db_schema."""
|
||||||
# Attempt to convert the table to utf8mb4
|
# Attempt to convert the table to utf8mb4
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Updating character set and collation of table %s to utf8mb4. %s",
|
"Updating table %s to character set %s and collation %s. %s",
|
||||||
table,
|
table,
|
||||||
|
MYSQL_DEFAULT_CHARSET,
|
||||||
|
MYSQL_COLLATE,
|
||||||
MIGRATION_NOTE_MINUTES,
|
MIGRATION_NOTE_MINUTES,
|
||||||
)
|
)
|
||||||
with (
|
with (
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["ical"],
|
"loggers": ["ical"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["ical==11.0.0"]
|
"requirements": ["ical==11.1.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ PLATFORMS = [
|
|||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
Platform.UPDATE,
|
Platform.UPDATE,
|
||||||
]
|
]
|
||||||
DEVICE_UPDATE_INTERVAL = timedelta(seconds=60)
|
DEVICE_UPDATE_INTERVAL_MIN = timedelta(seconds=60)
|
||||||
|
DEVICE_UPDATE_INTERVAL_PER_CAM = timedelta(seconds=10)
|
||||||
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=24)
|
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=24)
|
||||||
NUM_CRED_ERRORS = 3
|
NUM_CRED_ERRORS = 3
|
||||||
|
|
||||||
@@ -137,9 +138,12 @@ async def async_setup_entry(
|
|||||||
}
|
}
|
||||||
hass.config_entries.async_update_entry(config_entry, data=data)
|
hass.config_entries.async_update_entry(config_entry, data=data)
|
||||||
|
|
||||||
|
min_timeout = host.api.timeout * (RETRY_ATTEMPTS + 2)
|
||||||
|
update_timeout = max(min_timeout, min_timeout * host.api.num_cameras / 10)
|
||||||
|
|
||||||
async def async_device_config_update() -> None:
|
async def async_device_config_update() -> None:
|
||||||
"""Update the host state cache and renew the ONVIF-subscription."""
|
"""Update the host state cache and renew the ONVIF-subscription."""
|
||||||
async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
|
async with asyncio.timeout(update_timeout):
|
||||||
try:
|
try:
|
||||||
await host.update_states()
|
await host.update_states()
|
||||||
except CredentialsInvalidError as err:
|
except CredentialsInvalidError as err:
|
||||||
@@ -156,7 +160,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
host.credential_errors = 0
|
host.credential_errors = 0
|
||||||
|
|
||||||
async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
|
async with asyncio.timeout(min_timeout):
|
||||||
await host.renew()
|
await host.renew()
|
||||||
|
|
||||||
if host.api.new_devices and config_entry.state == ConfigEntryState.LOADED:
|
if host.api.new_devices and config_entry.state == ConfigEntryState.LOADED:
|
||||||
@@ -171,7 +175,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
async def async_check_firmware_update() -> None:
|
async def async_check_firmware_update() -> None:
|
||||||
"""Check for firmware updates."""
|
"""Check for firmware updates."""
|
||||||
async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
|
async with asyncio.timeout(min_timeout):
|
||||||
try:
|
try:
|
||||||
await host.api.check_new_firmware(host.firmware_ch_list)
|
await host.api.check_new_firmware(host.firmware_ch_list)
|
||||||
except ReolinkError as err:
|
except ReolinkError as err:
|
||||||
@@ -197,7 +201,10 @@ async def async_setup_entry(
|
|||||||
config_entry=config_entry,
|
config_entry=config_entry,
|
||||||
name=f"reolink.{host.api.nvr_name}",
|
name=f"reolink.{host.api.nvr_name}",
|
||||||
update_method=async_device_config_update,
|
update_method=async_device_config_update,
|
||||||
update_interval=DEVICE_UPDATE_INTERVAL,
|
update_interval=max(
|
||||||
|
DEVICE_UPDATE_INTERVAL_MIN,
|
||||||
|
DEVICE_UPDATE_INTERVAL_PER_CAM * host.api.num_cameras,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
firmware_coordinator = DataUpdateCoordinator(
|
firmware_coordinator = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
|
|||||||
@@ -19,5 +19,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["reolink_aio"],
|
"loggers": ["reolink_aio"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["reolink-aio==0.16.4"]
|
"requirements": ["reolink-aio==0.16.5"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.update_coordinator import (
|
|||||||
DataUpdateCoordinator,
|
DataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import DEVICE_UPDATE_INTERVAL
|
from . import DEVICE_UPDATE_INTERVAL_MIN, DEVICE_UPDATE_INTERVAL_PER_CAM
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import (
|
from .entity import (
|
||||||
ReolinkChannelCoordinatorEntity,
|
ReolinkChannelCoordinatorEntity,
|
||||||
@@ -221,7 +221,10 @@ class ReolinkUpdateBaseEntity(
|
|||||||
|
|
||||||
async def _resume_update_coordinator(self, *args: Any) -> None:
|
async def _resume_update_coordinator(self, *args: Any) -> None:
|
||||||
"""Resume updating the states using the data update coordinator (after reboots)."""
|
"""Resume updating the states using the data update coordinator (after reboots)."""
|
||||||
self._reolink_data.device_coordinator.update_interval = DEVICE_UPDATE_INTERVAL
|
self._reolink_data.device_coordinator.update_interval = max(
|
||||||
|
DEVICE_UPDATE_INTERVAL_MIN,
|
||||||
|
DEVICE_UPDATE_INTERVAL_PER_CAM * self._host.api.num_cameras,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
await self._reolink_data.device_coordinator.async_refresh()
|
await self._reolink_data.device_coordinator.async_refresh()
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Ruuvitag BLE device from a config entry."""
|
"""Set up Ruuvi BLE device from a config entry."""
|
||||||
address = entry.unique_id
|
address = entry.unique_id
|
||||||
assert address is not None
|
assert address is not None
|
||||||
data = RuuvitagBluetoothDeviceData()
|
data = RuuvitagBluetoothDeviceData()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"domain": "ruuvitag_ble",
|
"domain": "ruuvitag_ble",
|
||||||
"name": "RuuviTag BLE",
|
"name": "Ruuvi BLE",
|
||||||
"bluetooth": [
|
"bluetooth": [
|
||||||
{
|
{
|
||||||
"connectable": false,
|
"connectable": false,
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ async def async_setup_entry(
|
|||||||
entry: config_entries.ConfigEntry,
|
entry: config_entries.ConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Ruuvitag BLE sensors."""
|
"""Set up the Ruuvi BLE sensors."""
|
||||||
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
|
||||||
entry.entry_id
|
entry.entry_id
|
||||||
]
|
]
|
||||||
@@ -210,7 +210,7 @@ class RuuvitagBluetoothSensorEntity(
|
|||||||
],
|
],
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
):
|
):
|
||||||
"""Representation of a Ruuvitag BLE sensor."""
|
"""Representation of a Ruuvi BLE sensor."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> int | float | None:
|
def native_value(self) -> int | float | None:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user