mirror of
https://github.com/home-assistant/core.git
synced 2025-11-01 15:09:26 +00:00
Compare commits
213 Commits
2024.6.0b2
...
2024.6.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
394dafd980 | ||
|
|
eba429dc54 | ||
|
|
89ce8478de | ||
|
|
a4a8315376 | ||
|
|
3a705fd668 | ||
|
|
dc0fc318b8 | ||
|
|
5ceb8537eb | ||
|
|
d7d7782a69 | ||
|
|
2d4176d581 | ||
|
|
204e9a79c5 | ||
|
|
ace7da2328 | ||
|
|
dfe25ff804 | ||
|
|
2b44cf898e | ||
|
|
c77ed921de | ||
|
|
78e13d138f | ||
|
|
4e394597bd | ||
|
|
78c2dc708c | ||
|
|
4c1d2e7ac8 | ||
|
|
7b809a8e55 | ||
|
|
4eea448f9d | ||
|
|
f58882c878 | ||
|
|
4e6e9f35b5 | ||
|
|
d5e9976b2c | ||
|
|
8d547d4599 | ||
|
|
94d79440a0 | ||
|
|
d602b7d19b | ||
|
|
fb5de55c3e | ||
|
|
5cf0ee936d | ||
|
|
7443878333 | ||
|
|
090d296135 | ||
|
|
415bfb40a7 | ||
|
|
7ced4e981e | ||
|
|
b656ef4d4f | ||
|
|
6ea18a7b24 | ||
|
|
a0ac9fe6c9 | ||
|
|
135735126a | ||
|
|
3bc6cf666a | ||
|
|
1929e103c0 | ||
|
|
74b49556f9 | ||
|
|
8d40f4d39f | ||
|
|
eed126c6d4 | ||
|
|
38cd84fa5f | ||
|
|
a28f5baeeb | ||
|
|
f9352dfe8f | ||
|
|
5beff34069 | ||
|
|
119d4c2316 | ||
|
|
1e7ab07d9e | ||
|
|
7896e7675c | ||
|
|
8b415a0376 | ||
|
|
6a656c5d49 | ||
|
|
8d094bf12e | ||
|
|
c71b6bdac9 | ||
|
|
57cc1f841b | ||
|
|
d8f3778d77 | ||
|
|
9a8e3ad5cc | ||
|
|
019d33c06c | ||
|
|
40ebf3b2a9 | ||
|
|
7912c9e95c | ||
|
|
4bb1ea1da1 | ||
|
|
a696ea18d3 | ||
|
|
df96b94985 | ||
|
|
0f8ed4e73d | ||
|
|
34477d3559 | ||
|
|
96ac566032 | ||
|
|
87f48b15d1 | ||
|
|
a1f2140ed7 | ||
|
|
db7a9321be | ||
|
|
ebb0a453f4 | ||
|
|
7da10794a8 | ||
|
|
461f0865af | ||
|
|
fc83bb1737 | ||
|
|
b28cdcfc49 | ||
|
|
3f70e2b6f0 | ||
|
|
ed22e98861 | ||
|
|
093f07c04e | ||
|
|
b5693ca604 | ||
|
|
20b77aa15f | ||
|
|
1cbd3ab930 | ||
|
|
31b44b7846 | ||
|
|
de3a0841d8 | ||
|
|
581fb2f9f4 | ||
|
|
5bb4e4f5d9 | ||
|
|
cfa619b67e | ||
|
|
56db7fc7dc | ||
|
|
1f6be7b4d1 | ||
|
|
52d1432d81 | ||
|
|
14da1e9b23 | ||
|
|
d6e1d05e87 | ||
|
|
62f73cfcca | ||
|
|
6e9a53d02e | ||
|
|
394c13af1d | ||
|
|
86b13e8ae3 | ||
|
|
5a7332a135 | ||
|
|
0f9a91d369 | ||
|
|
00dd86fb4b | ||
|
|
460909a7f6 | ||
|
|
21fd012447 | ||
|
|
c27f0c560e | ||
|
|
0f4a1b421e | ||
|
|
5e35ce2996 | ||
|
|
e5804307e7 | ||
|
|
3b74b63b23 | ||
|
|
06df32d9d4 | ||
|
|
63947e4980 | ||
|
|
ac6a377478 | ||
|
|
18af423a78 | ||
|
|
f1445bc8f5 | ||
|
|
3784c99305 | ||
|
|
0084d6c5bd | ||
|
|
f1e6375406 | ||
|
|
9157905f80 | ||
|
|
b02c9aa2ef | ||
|
|
6e30fd7633 | ||
|
|
74b29c2e54 | ||
|
|
b1b26af92b | ||
|
|
b107ffd30d | ||
|
|
776675404a | ||
|
|
38ee32fed2 | ||
|
|
111d11aaca | ||
|
|
ff8752ea4f | ||
|
|
2151f7ebf3 | ||
|
|
50efce4e53 | ||
|
|
c8538f3c08 | ||
|
|
4bfff12570 | ||
|
|
f2b1635969 | ||
|
|
b3b8ae31fd | ||
|
|
ba96fc272b | ||
|
|
c702174fa0 | ||
|
|
5d6fe7387e | ||
|
|
c76b7a48d3 | ||
|
|
954e8ff9b3 | ||
|
|
8c332ddbdb | ||
|
|
01c4ca2749 | ||
|
|
4b4b5362d9 | ||
|
|
70d7cedf08 | ||
|
|
7bbfb1a22b | ||
|
|
d68d871054 | ||
|
|
69bdefb02d | ||
|
|
ebaec6380f | ||
|
|
9cf6e9b21a | ||
|
|
eb1a9eda60 | ||
|
|
26344ffd74 | ||
|
|
2940104008 | ||
|
|
8072a268a1 | ||
|
|
b5f557ad73 | ||
|
|
f977b54312 | ||
|
|
11b2f201f3 | ||
|
|
8cc3c147fe | ||
|
|
fd9ea2f224 | ||
|
|
f064f44a09 | ||
|
|
f3d1157bc4 | ||
|
|
85982d2b87 | ||
|
|
cc83443ad1 | ||
|
|
8a516207e9 | ||
|
|
f805df8390 | ||
|
|
ea85ed6992 | ||
|
|
54425b756e | ||
|
|
7b43b587a7 | ||
|
|
7e71975358 | ||
|
|
e0232510d7 | ||
|
|
84f9bb1d63 | ||
|
|
b436fe94ae | ||
|
|
aff5da5762 | ||
|
|
b5783e6f5c | ||
|
|
1708b60ecf | ||
|
|
3c012c497b | ||
|
|
4d2dc9a40e | ||
|
|
3653a51288 | ||
|
|
9366a4e69b | ||
|
|
1d1af7ec11 | ||
|
|
236b19c5b3 | ||
|
|
1afbfd687f | ||
|
|
20159d0277 | ||
|
|
4df3d43e45 | ||
|
|
1a588760b9 | ||
|
|
6ba9e7d5fd | ||
|
|
4b06c5d2fb | ||
|
|
bfc1c62a49 | ||
|
|
c52fabcf77 | ||
|
|
b39d7b39e1 | ||
|
|
c01c155037 | ||
|
|
b459559c8b | ||
|
|
d823e56659 | ||
|
|
e401a0da7f | ||
|
|
3f6df28ef3 | ||
|
|
9b63779063 | ||
|
|
4998fe5e6d | ||
|
|
a59c890779 | ||
|
|
a2cdb349f4 | ||
|
|
267228cae0 | ||
|
|
ba769f4d9f | ||
|
|
c09bc726d1 | ||
|
|
c441f689bf | ||
|
|
395e1ae31e | ||
|
|
2e45d678b8 | ||
|
|
17cb25a5b6 | ||
|
|
e5e26de06f | ||
|
|
7dab255c15 | ||
|
|
cea7347ed9 | ||
|
|
f4a876c590 | ||
|
|
117a02972d | ||
|
|
3fb40deacb | ||
|
|
38c88c576b | ||
|
|
e95b63bc89 | ||
|
|
ea44b534e6 | ||
|
|
7646d853f4 | ||
|
|
248c7c33b2 | ||
|
|
eb887a707c | ||
|
|
e3ddbb2768 | ||
|
|
008aec5670 | ||
|
|
d93d7159db | ||
|
|
e6e017dab7 | ||
|
|
486c72db73 |
@@ -62,7 +62,6 @@ omit =
|
||||
homeassistant/components/aladdin_connect/api.py
|
||||
homeassistant/components/aladdin_connect/application_credentials.py
|
||||
homeassistant/components/aladdin_connect/cover.py
|
||||
homeassistant/components/aladdin_connect/model.py
|
||||
homeassistant/components/aladdin_connect/sensor.py
|
||||
homeassistant/components/alarmdecoder/__init__.py
|
||||
homeassistant/components/alarmdecoder/alarm_control_panel.py
|
||||
@@ -1534,7 +1533,6 @@ omit =
|
||||
homeassistant/components/v2c/coordinator.py
|
||||
homeassistant/components/v2c/entity.py
|
||||
homeassistant/components/v2c/number.py
|
||||
homeassistant/components/v2c/sensor.py
|
||||
homeassistant/components/v2c/switch.py
|
||||
homeassistant/components/vallox/__init__.py
|
||||
homeassistant/components/vallox/coordinator.py
|
||||
|
||||
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: core
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
DEFAULT_PYTHON: "3.12.3"
|
||||
PIP_TIMEOUT: 60
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
|
||||
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -37,8 +37,8 @@ env:
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 8
|
||||
HA_SHORT_VERSION: "2024.6"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||
DEFAULT_PYTHON: "3.12.3"
|
||||
ALL_PYTHON_VERSIONS: "['3.12.3']"
|
||||
# 10.3 is the oldest supported version
|
||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||
# 10.6 is the current long-term-support
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
- "**strings.json"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.11"
|
||||
DEFAULT_PYTHON: "3.12.3"
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
|
||||
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
@@ -17,7 +17,7 @@ on:
|
||||
- "script/gen_requirements_all.py"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
DEFAULT_PYTHON: "3.12.3"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name}}
|
||||
|
||||
@@ -163,7 +163,6 @@ homeassistant.components.easyenergy.*
|
||||
homeassistant.components.ecovacs.*
|
||||
homeassistant.components.ecowitt.*
|
||||
homeassistant.components.efergy.*
|
||||
homeassistant.components.electrasmart.*
|
||||
homeassistant.components.electric_kiwi.*
|
||||
homeassistant.components.elgato.*
|
||||
homeassistant.components.elkm1.*
|
||||
|
||||
@@ -1486,8 +1486,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/unifi/ @Kane610
|
||||
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
||||
/homeassistant/components/unifiled/ @florisvdk
|
||||
/homeassistant/components/unifiprotect/ @bdraco
|
||||
/tests/components/unifiprotect/ @bdraco
|
||||
/homeassistant/components/upb/ @gwww
|
||||
/tests/components/upb/ @gwww
|
||||
/homeassistant/components/upc_connect/ @pvizeli @fabaff
|
||||
|
||||
@@ -134,8 +134,15 @@ COOLDOWN_TIME = 60
|
||||
|
||||
|
||||
DEBUGGER_INTEGRATIONS = {"debugpy"}
|
||||
|
||||
# Core integrations are unconditionally loaded
|
||||
CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"}
|
||||
LOGGING_INTEGRATIONS = {
|
||||
|
||||
# Integrations that are loaded right after the core is set up
|
||||
LOGGING_AND_HTTP_DEPS_INTEGRATIONS = {
|
||||
# isal is loaded right away before `http` to ensure if its
|
||||
# enabled, that `isal` is up to date.
|
||||
"isal",
|
||||
# Set log levels
|
||||
"logger",
|
||||
# Error logging
|
||||
@@ -214,8 +221,8 @@ CRITICAL_INTEGRATIONS = {
|
||||
}
|
||||
|
||||
SETUP_ORDER = (
|
||||
# Load logging as soon as possible
|
||||
("logging", LOGGING_INTEGRATIONS),
|
||||
# Load logging and http deps as soon as possible
|
||||
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
|
||||
# Setup frontend and recorder
|
||||
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}),
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ads",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyads"],
|
||||
"requirements": ["pyads==3.2.2"]
|
||||
"requirements": ["pyads==3.4.0"]
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class AgentBaseStation(AlarmControlPanelEntity):
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
| AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||
)
|
||||
_attr_code_arm_required = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from airgradient import AirGradientClient, AirGradientError
|
||||
from airgradient import AirGradientClient, AirGradientError, ConfigurationControl
|
||||
from awesomeversion import AwesomeVersion
|
||||
from mashumaro import MissingField
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
@@ -12,6 +14,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
MIN_VERSION = AwesomeVersion("3.1.1")
|
||||
|
||||
|
||||
class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""AirGradient config flow."""
|
||||
@@ -19,6 +23,14 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self.data: dict[str, Any] = {}
|
||||
self.client: AirGradientClient | None = None
|
||||
|
||||
async def set_configuration_source(self) -> None:
|
||||
"""Set configuration source to local if it hasn't been set yet."""
|
||||
assert self.client
|
||||
config = await self.client.get_config()
|
||||
if config.configuration_control is ConfigurationControl.NOT_INITIALIZED:
|
||||
await self.client.set_configuration_control(ConfigurationControl.LOCAL)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
@@ -30,9 +42,12 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(discovery_info.properties["serialno"])
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||
|
||||
if AwesomeVersion(discovery_info.properties["fw_ver"]) < MIN_VERSION:
|
||||
return self.async_abort(reason="invalid_version")
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
air_gradient = AirGradientClient(host, session=session)
|
||||
await air_gradient.get_current_measures()
|
||||
self.client = AirGradientClient(host, session=session)
|
||||
await self.client.get_current_measures()
|
||||
|
||||
self.context["title_placeholders"] = {
|
||||
"model": self.data[CONF_MODEL],
|
||||
@@ -44,6 +59,7 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm discovery."""
|
||||
if user_input is not None:
|
||||
await self.set_configuration_source()
|
||||
return self.async_create_entry(
|
||||
title=self.data[CONF_MODEL],
|
||||
data={CONF_HOST: self.data[CONF_HOST]},
|
||||
@@ -64,14 +80,17 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors: dict[str, str] = {}
|
||||
if user_input:
|
||||
session = async_get_clientsession(self.hass)
|
||||
air_gradient = AirGradientClient(user_input[CONF_HOST], session=session)
|
||||
self.client = AirGradientClient(user_input[CONF_HOST], session=session)
|
||||
try:
|
||||
current_measures = await air_gradient.get_current_measures()
|
||||
current_measures = await self.client.get_current_measures()
|
||||
except AirGradientError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except MissingField:
|
||||
return self.async_abort(reason="invalid_version")
|
||||
else:
|
||||
await self.async_set_unique_id(current_measures.serial_number)
|
||||
self._abort_if_unique_id_configured()
|
||||
await self.set_configuration_source()
|
||||
return self.async_create_entry(
|
||||
title=current_measures.model,
|
||||
data={CONF_HOST: user_input[CONF_HOST]},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"domain": "airgradient",
|
||||
"name": "Airgradient",
|
||||
"name": "AirGradient",
|
||||
"codeowners": ["@airgradienthq", "@joostlek"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airgradient==0.4.2"],
|
||||
"requirements": ["airgradient==0.4.3"],
|
||||
"zeroconf": ["_airgradient._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ from .entity import AirGradientEntity
|
||||
class AirGradientSelectEntityDescription(SelectEntityDescription):
|
||||
"""Describes AirGradient select entity."""
|
||||
|
||||
value_fn: Callable[[Config], str]
|
||||
value_fn: Callable[[Config], str | None]
|
||||
set_value_fn: Callable[[AirGradientClient, str], Awaitable[None]]
|
||||
requires_display: bool = False
|
||||
|
||||
@@ -30,9 +30,11 @@ class AirGradientSelectEntityDescription(SelectEntityDescription):
|
||||
CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription(
|
||||
key="configuration_control",
|
||||
translation_key="configuration_control",
|
||||
options=[x.value for x in ConfigurationControl],
|
||||
options=[ConfigurationControl.CLOUD.value, ConfigurationControl.LOCAL.value],
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda config: config.configuration_control,
|
||||
value_fn=lambda config: config.configuration_control
|
||||
if config.configuration_control is not ConfigurationControl.NOT_INITIALIZED
|
||||
else None,
|
||||
set_value_fn=lambda client, value: client.set_configuration_control(
|
||||
ConfigurationControl(value)
|
||||
),
|
||||
@@ -96,7 +98,7 @@ class AirGradientSelect(AirGradientEntity, SelectEntity):
|
||||
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||
|
||||
@property
|
||||
def current_option(self) -> str:
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the state of the select."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = (
|
||||
AirGradientSensorEntityDescription(
|
||||
key="pm003",
|
||||
translation_key="pm003_count",
|
||||
native_unit_of_measurement="particles/dL",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda status: status.pm003_count,
|
||||
),
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -28,8 +29,7 @@
|
||||
"name": "Configuration source",
|
||||
"state": {
|
||||
"cloud": "Cloud",
|
||||
"local": "Local",
|
||||
"both": "Both"
|
||||
"local": "Local"
|
||||
}
|
||||
},
|
||||
"display_temperature_unit": {
|
||||
@@ -48,7 +48,7 @@
|
||||
"name": "Nitrogen index"
|
||||
},
|
||||
"pm003_count": {
|
||||
"name": "PM0.3 count"
|
||||
"name": "PM0.3"
|
||||
},
|
||||
"raw_total_volatile_organic_component": {
|
||||
"name": "Raw total VOC"
|
||||
|
||||
@@ -2,52 +2,93 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from genie_partner_sdk.client import AladdinConnectClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
|
||||
from . import api
|
||||
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION
|
||||
from .api import AsyncConfigEntryAuth
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AladdinConnectCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.COVER]
|
||||
PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR]
|
||||
|
||||
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Aladdin Connect Genie from a config entry."""
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session)
|
||||
coordinator = AladdinConnectCoordinator(hass, AladdinConnectClient(auth))
|
||||
|
||||
# If using an aiohttp-based API lib
|
||||
entry.runtime_data = api.AsyncConfigEntryAuth(
|
||||
aiohttp_client.async_get_clientsession(hass), session
|
||||
)
|
||||
await coordinator.async_setup()
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
async_remove_stale_devices(hass, entry)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate old config."""
|
||||
if config_entry.version < CONFIG_FLOW_VERSION:
|
||||
if config_entry.version < 2:
|
||||
config_entry.async_start_reauth(hass)
|
||||
new_data = {**config_entry.data}
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data=new_data,
|
||||
version=CONFIG_FLOW_VERSION,
|
||||
minor_version=CONFIG_FLOW_MINOR_VERSION,
|
||||
version=2,
|
||||
minor_version=1,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def async_remove_stale_devices(
|
||||
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
|
||||
) -> None:
|
||||
"""Remove stale devices from device registry."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry.entry_id
|
||||
)
|
||||
all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors}
|
||||
|
||||
for device_entry in device_entries:
|
||||
device_id: str | None = None
|
||||
|
||||
for identifier in device_entry.identifiers:
|
||||
if identifier[0] == DOMAIN:
|
||||
device_id = identifier[1]
|
||||
break
|
||||
|
||||
if device_id is None or device_id not in all_device_ids:
|
||||
# If device_id is None an invalid device entry was found for this config entry.
|
||||
# If the device_id is not in existing device ids it's a stale device entry.
|
||||
# Remove config entry from this device entry in either case.
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, remove_config_entry_id=config_entry.entry_id
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""API for Aladdin Connect Genie bound to Home Assistant OAuth."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from genie_partner_sdk.auth import Auth
|
||||
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
|
||||
API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1"
|
||||
API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3"
|
||||
@@ -15,7 +17,7 @@ class AsyncConfigEntryAuth(Auth): # type: ignore[misc]
|
||||
def __init__(
|
||||
self,
|
||||
websession: ClientSession,
|
||||
oauth_session: config_entry_oauth2_flow.OAuth2Session,
|
||||
oauth_session: OAuth2Session,
|
||||
) -> None:
|
||||
"""Initialize Aladdin Connect Genie auth."""
|
||||
super().__init__(
|
||||
@@ -25,7 +27,6 @@ class AsyncConfigEntryAuth(Auth): # type: ignore[misc]
|
||||
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Return a valid access token."""
|
||||
if not self._oauth_session.valid_token:
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
|
||||
return str(self._oauth_session.token["access_token"])
|
||||
return cast(str, self._oauth_session.token["access_token"])
|
||||
|
||||
@@ -4,22 +4,21 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
import jwt
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
|
||||
|
||||
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class OAuth2FlowHandler(
|
||||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
|
||||
):
|
||||
class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Config flow to handle Aladdin Connect Genie OAuth2 authentication."""
|
||||
|
||||
DOMAIN = DOMAIN
|
||||
VERSION = CONFIG_FLOW_VERSION
|
||||
MINOR_VERSION = CONFIG_FLOW_MINOR_VERSION
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 1
|
||||
|
||||
reauth_entry: ConfigEntry | None = None
|
||||
|
||||
@@ -37,20 +36,33 @@ class OAuth2FlowHandler(
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema({}),
|
||||
)
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Create an oauth config entry or update existing entry for reauth."""
|
||||
if self.reauth_entry:
|
||||
token_payload = jwt.decode(
|
||||
data[CONF_TOKEN][CONF_ACCESS_TOKEN], options={"verify_signature": False}
|
||||
)
|
||||
if not self.reauth_entry:
|
||||
await self.async_set_unique_id(token_payload["sub"])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=token_payload["username"],
|
||||
data=data,
|
||||
)
|
||||
|
||||
if self.reauth_entry.unique_id == token_payload["username"]:
|
||||
return self.async_update_reload_and_abort(
|
||||
self.reauth_entry,
|
||||
data=data,
|
||||
unique_id=token_payload["sub"],
|
||||
)
|
||||
return await super().async_oauth_create_entry(data)
|
||||
if self.reauth_entry.unique_id == token_payload["sub"]:
|
||||
return self.async_update_reload_and_abort(self.reauth_entry, data=data)
|
||||
|
||||
return self.async_abort(reason="wrong_account")
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
"""Constants for the Aladdin Connect Genie integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.cover import CoverEntityFeature
|
||||
|
||||
DOMAIN = "aladdin_connect"
|
||||
CONFIG_FLOW_VERSION = 2
|
||||
CONFIG_FLOW_MINOR_VERSION = 1
|
||||
|
||||
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html"
|
||||
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html"
|
||||
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"
|
||||
|
||||
SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
||||
|
||||
38
homeassistant/components/aladdin_connect/coordinator.py
Normal file
38
homeassistant/components/aladdin_connect/coordinator.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Define an object to coordinate fetching Aladdin Connect data."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from genie_partner_sdk.client import AladdinConnectClient
|
||||
from genie_partner_sdk.model import GarageDoor
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AladdinConnectCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Aladdin Connect Data Update Coordinator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, acc: AladdinConnectClient) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=15),
|
||||
)
|
||||
self.acc = acc
|
||||
self.doors: list[GarageDoor] = []
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Fetch initial data."""
|
||||
self.doors = await self.acc.get_doors()
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from API endpoint."""
|
||||
for door in self.doors:
|
||||
await self.acc.update_door(door.device_id, door.door_number)
|
||||
@@ -1,115 +1,64 @@
|
||||
"""Cover Entity for Genie Garage Door."""
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from genie_partner_sdk.client import AladdinConnectClient
|
||||
from genie_partner_sdk.model import GarageDoor
|
||||
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.components.cover import (
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import api
|
||||
from .const import DOMAIN, SUPPORTED_FEATURES
|
||||
from .model import GarageDoor
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
from . import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
||||
from .entity import AladdinConnectEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AladdinConnectConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Aladdin Connect platform."""
|
||||
session: api.AsyncConfigEntryAuth = config_entry.runtime_data
|
||||
acc = AladdinConnectClient(session)
|
||||
doors = await acc.get_doors()
|
||||
if doors is None:
|
||||
raise PlatformNotReady("Error from Aladdin Connect getting doors")
|
||||
device_registry = dr.async_get(hass)
|
||||
doors_to_add = []
|
||||
for door in doors:
|
||||
existing = device_registry.async_get(door.unique_id)
|
||||
if existing is None:
|
||||
doors_to_add.append(door)
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
(AladdinDevice(acc, door, config_entry) for door in doors_to_add),
|
||||
)
|
||||
remove_stale_devices(hass, config_entry, doors)
|
||||
async_add_entities(AladdinDevice(coordinator, door) for door in coordinator.doors)
|
||||
|
||||
|
||||
def remove_stale_devices(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, devices: list[GarageDoor]
|
||||
) -> None:
|
||||
"""Remove stale devices from device registry."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry.entry_id
|
||||
)
|
||||
all_device_ids = {door.unique_id for door in devices}
|
||||
|
||||
for device_entry in device_entries:
|
||||
device_id: str | None = None
|
||||
|
||||
for identifier in device_entry.identifiers:
|
||||
if identifier[0] == DOMAIN:
|
||||
device_id = identifier[1]
|
||||
break
|
||||
|
||||
if device_id is None or device_id not in all_device_ids:
|
||||
# If device_id is None an invalid device entry was found for this config entry.
|
||||
# If the device_id is not in existing device ids it's a stale device entry.
|
||||
# Remove config entry from this device entry in either case.
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, remove_config_entry_id=config_entry.entry_id
|
||||
)
|
||||
|
||||
|
||||
class AladdinDevice(CoverEntity):
|
||||
class AladdinDevice(AladdinConnectEntity, CoverEntity):
|
||||
"""Representation of Aladdin Connect cover."""
|
||||
|
||||
_attr_device_class = CoverDeviceClass.GARAGE
|
||||
_attr_supported_features = SUPPORTED_FEATURES
|
||||
_attr_has_entity_name = True
|
||||
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self, acc: AladdinConnectClient, device: GarageDoor, entry: ConfigEntry
|
||||
self, coordinator: AladdinConnectCoordinator, device: GarageDoor
|
||||
) -> None:
|
||||
"""Initialize the Aladdin Connect cover."""
|
||||
self._acc = acc
|
||||
self._device_id = device.device_id
|
||||
self._number = device.door_number
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.unique_id)},
|
||||
name=device.name,
|
||||
manufacturer="Overhead Door",
|
||||
)
|
||||
super().__init__(coordinator, device)
|
||||
self._attr_unique_id = device.unique_id
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Issue open command to cover."""
|
||||
await self._acc.open_door(self._device_id, self._number)
|
||||
await self.coordinator.acc.open_door(
|
||||
self._device.device_id, self._device.door_number
|
||||
)
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Issue close command to cover."""
|
||||
await self._acc.close_door(self._device_id, self._number)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update status of cover."""
|
||||
await self._acc.update_door(self._device_id, self._number)
|
||||
await self.coordinator.acc.close_door(
|
||||
self._device.device_id, self._device.door_number
|
||||
)
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
"""Update is closed attribute."""
|
||||
value = self._acc.get_door_status(self._device_id, self._number)
|
||||
value = self.coordinator.acc.get_door_status(
|
||||
self._device.device_id, self._device.door_number
|
||||
)
|
||||
if value is None:
|
||||
return None
|
||||
return bool(value == "closed")
|
||||
@@ -117,7 +66,9 @@ class AladdinDevice(CoverEntity):
|
||||
@property
|
||||
def is_closing(self) -> bool | None:
|
||||
"""Update is closing attribute."""
|
||||
value = self._acc.get_door_status(self._device_id, self._number)
|
||||
value = self.coordinator.acc.get_door_status(
|
||||
self._device.device_id, self._device.door_number
|
||||
)
|
||||
if value is None:
|
||||
return None
|
||||
return bool(value == "closing")
|
||||
@@ -125,7 +76,9 @@ class AladdinDevice(CoverEntity):
|
||||
@property
|
||||
def is_opening(self) -> bool | None:
|
||||
"""Update is opening attribute."""
|
||||
value = self._acc.get_door_status(self._device_id, self._number)
|
||||
value = self.coordinator.acc.get_door_status(
|
||||
self._device.device_id, self._device.door_number
|
||||
)
|
||||
if value is None:
|
||||
return None
|
||||
return bool(value == "opening")
|
||||
|
||||
27
homeassistant/components/aladdin_connect/entity.py
Normal file
27
homeassistant/components/aladdin_connect/entity.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Defines a base Aladdin Connect entity."""
|
||||
|
||||
from genie_partner_sdk.model import GarageDoor
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AladdinConnectCoordinator
|
||||
|
||||
|
||||
class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
|
||||
"""Defines a base Aladdin Connect entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, coordinator: AladdinConnectCoordinator, device: GarageDoor
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._device = device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.unique_id)},
|
||||
name=device.name,
|
||||
manufacturer="Overhead Door",
|
||||
)
|
||||
@@ -1,30 +0,0 @@
|
||||
"""Models for Aladdin connect cover platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class GarageDoorData(TypedDict):
|
||||
"""Aladdin door data."""
|
||||
|
||||
device_id: str
|
||||
door_number: int
|
||||
name: str
|
||||
status: str
|
||||
link_status: str
|
||||
battery_level: int
|
||||
|
||||
|
||||
class GarageDoor:
|
||||
"""Aladdin Garage Door Entity."""
|
||||
|
||||
def __init__(self, data: GarageDoorData) -> None:
|
||||
"""Create `GarageDoor` from dictionary of data."""
|
||||
self.device_id = data["device_id"]
|
||||
self.door_number = data["door_number"]
|
||||
self.unique_id = f"{self.device_id}-{self.door_number}"
|
||||
self.name = data["name"]
|
||||
self.status = data["status"]
|
||||
self.link_status = data["link_status"]
|
||||
self.battery_level = data["battery_level"]
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from genie_partner_sdk.client import AladdinConnectClient
|
||||
from genie_partner_sdk.model import GarageDoor
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -14,22 +14,19 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import api
|
||||
from .const import DOMAIN
|
||||
from .model import GarageDoor
|
||||
from . import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
||||
from .entity import AladdinConnectEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AccSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes AladdinConnect sensor entity."""
|
||||
|
||||
value_fn: Callable
|
||||
value_fn: Callable[[AladdinConnectClient, str, int], float | None]
|
||||
|
||||
|
||||
SENSORS: tuple[AccSensorEntityDescription, ...] = (
|
||||
@@ -45,52 +42,39 @@ SENSORS: tuple[AccSensorEntityDescription, ...] = (
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: AladdinConnectConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Aladdin Connect sensor devices."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
session: api.AsyncConfigEntryAuth = hass.data[DOMAIN][entry.entry_id]
|
||||
acc = AladdinConnectClient(session)
|
||||
|
||||
entities = []
|
||||
doors = await acc.get_doors()
|
||||
|
||||
for door in doors:
|
||||
entities.extend(
|
||||
[AladdinConnectSensor(acc, door, description) for description in SENSORS]
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(
|
||||
AladdinConnectSensor(coordinator, door, description)
|
||||
for description in SENSORS
|
||||
for door in coordinator.doors
|
||||
)
|
||||
|
||||
|
||||
class AladdinConnectSensor(SensorEntity):
|
||||
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
|
||||
"""A sensor implementation for Aladdin Connect devices."""
|
||||
|
||||
entity_description: AccSensorEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
acc: AladdinConnectClient,
|
||||
coordinator: AladdinConnectCoordinator,
|
||||
device: GarageDoor,
|
||||
description: AccSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a sensor for an Aladdin Connect device."""
|
||||
self._device_id = device.device_id
|
||||
self._number = device.door_number
|
||||
self._acc = acc
|
||||
super().__init__(coordinator, device)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device.unique_id}-{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.unique_id)},
|
||||
name=device.name,
|
||||
manufacturer="Overhead Door",
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
return cast(
|
||||
float,
|
||||
self.entity_description.value_fn(self._acc, self._device_id, self._number),
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.acc, self._device.device_id, self._device.door_number
|
||||
)
|
||||
|
||||
@@ -62,13 +62,12 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
|
||||
|
||||
Adds an empty filter to hass data.
|
||||
Tries to get a filter from yaml, if present set to hass data.
|
||||
If config is empty after getting the filter, return, otherwise emit
|
||||
deprecated warning and pass the rest to the config flow.
|
||||
"""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {DATA_FILTER: {}})
|
||||
hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})})
|
||||
if DOMAIN in yaml_config:
|
||||
hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN][CONF_FILTER]
|
||||
hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN].pop(CONF_FILTER)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -207,6 +206,6 @@ class AzureDataExplorer:
|
||||
if "\n" in state.state:
|
||||
return None, dropped + 1
|
||||
|
||||
json_event = str(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8"))
|
||||
json_event = json.dumps(obj=state, cls=JSONEncoder)
|
||||
|
||||
return (json_event, dropped)
|
||||
|
||||
@@ -23,7 +23,7 @@ from .const import (
|
||||
CONF_APP_REG_ID,
|
||||
CONF_APP_REG_SECRET,
|
||||
CONF_AUTHORITY_ID,
|
||||
CONF_USE_FREE,
|
||||
CONF_USE_QUEUED_CLIENT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,7 +35,6 @@ class AzureDataExplorerClient:
|
||||
def __init__(self, data: Mapping[str, Any]) -> None:
|
||||
"""Create the right class."""
|
||||
|
||||
self._cluster_ingest_uri = data[CONF_ADX_CLUSTER_INGEST_URI]
|
||||
self._database = data[CONF_ADX_DATABASE_NAME]
|
||||
self._table = data[CONF_ADX_TABLE_NAME]
|
||||
self._ingestion_properties = IngestionProperties(
|
||||
@@ -45,24 +44,36 @@ class AzureDataExplorerClient:
|
||||
ingestion_mapping_reference="ha_json_mapping",
|
||||
)
|
||||
|
||||
# Create cLient for ingesting and querying data
|
||||
kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
||||
self._cluster_ingest_uri,
|
||||
data[CONF_APP_REG_ID],
|
||||
data[CONF_APP_REG_SECRET],
|
||||
data[CONF_AUTHORITY_ID],
|
||||
# Create client for ingesting data
|
||||
kcsb_ingest = (
|
||||
KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
||||
data[CONF_ADX_CLUSTER_INGEST_URI],
|
||||
data[CONF_APP_REG_ID],
|
||||
data[CONF_APP_REG_SECRET],
|
||||
data[CONF_AUTHORITY_ID],
|
||||
)
|
||||
)
|
||||
|
||||
if data[CONF_USE_FREE] is True:
|
||||
# Queded is the only option supported on free tear of ADX
|
||||
self.write_client = QueuedIngestClient(kcsb)
|
||||
else:
|
||||
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb)
|
||||
# Create client for querying data
|
||||
kcsb_query = (
|
||||
KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
||||
data[CONF_ADX_CLUSTER_INGEST_URI].replace("ingest-", ""),
|
||||
data[CONF_APP_REG_ID],
|
||||
data[CONF_APP_REG_SECRET],
|
||||
data[CONF_AUTHORITY_ID],
|
||||
)
|
||||
)
|
||||
|
||||
self.query_client = KustoClient(kcsb)
|
||||
if data[CONF_USE_QUEUED_CLIENT] is True:
|
||||
# Queded is the only option supported on free tear of ADX
|
||||
self.write_client = QueuedIngestClient(kcsb_ingest)
|
||||
else:
|
||||
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb_ingest)
|
||||
|
||||
self.query_client = KustoClient(kcsb_query)
|
||||
|
||||
def test_connection(self) -> None:
|
||||
"""Test connection, will throw Exception when it cannot connect."""
|
||||
"""Test connection, will throw Exception if it cannot connect."""
|
||||
|
||||
query = f"{self._table} | take 1"
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.helpers.selector import BooleanSelector
|
||||
|
||||
from . import AzureDataExplorerClient
|
||||
from .const import (
|
||||
@@ -19,7 +20,7 @@ from .const import (
|
||||
CONF_APP_REG_ID,
|
||||
CONF_APP_REG_SECRET,
|
||||
CONF_AUTHORITY_ID,
|
||||
CONF_USE_FREE,
|
||||
CONF_USE_QUEUED_CLIENT,
|
||||
DEFAULT_OPTIONS,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -34,7 +35,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_APP_REG_ID): str,
|
||||
vol.Required(CONF_APP_REG_SECRET): str,
|
||||
vol.Required(CONF_AUTHORITY_ID): str,
|
||||
vol.Optional(CONF_USE_FREE, default=False): bool,
|
||||
vol.Required(CONF_USE_QUEUED_CLIENT, default=False): BooleanSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ CONF_AUTHORITY_ID = "authority_id"
|
||||
CONF_SEND_INTERVAL = "send_interval"
|
||||
CONF_MAX_DELAY = "max_delay"
|
||||
CONF_FILTER = DATA_FILTER = "filter"
|
||||
CONF_USE_FREE = "use_queued_ingestion"
|
||||
CONF_USE_QUEUED_CLIENT = "use_queued_ingestion"
|
||||
DATA_HUB = "hub"
|
||||
STEP_USER = "user"
|
||||
|
||||
|
||||
@@ -3,14 +3,19 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Setup your Azure Data Explorer integration",
|
||||
"description": "Enter connection details.",
|
||||
"description": "Enter connection details",
|
||||
"data": {
|
||||
"clusteringesturi": "Cluster Ingest URI",
|
||||
"database": "Database name",
|
||||
"table": "Table name",
|
||||
"cluster_ingest_uri": "Cluster Ingest URI",
|
||||
"authority_id": "Authority ID",
|
||||
"client_id": "Client ID",
|
||||
"client_secret": "Client secret",
|
||||
"authority_id": "Authority ID"
|
||||
"database": "Database name",
|
||||
"table": "Table name",
|
||||
"use_queued_ingestion": "Use queued ingestion"
|
||||
},
|
||||
"data_description": {
|
||||
"cluster_ingest_uri": "Ingest-URI of the cluster",
|
||||
"use_queued_ingestion": "Must be enabled when using ADX free cluster"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -46,6 +46,7 @@ class BlinkSyncModuleHA(
|
||||
"""Representation of a Blink Alarm Control Panel."""
|
||||
|
||||
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
_attr_code_arm_required = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
},
|
||||
"save_recent_clips": {
|
||||
"name": "Save recent clips",
|
||||
"description": "Saves all recent video clips to local directory with file pattern \"%Y%m%d_%H%M%S_{name}.mp4\".",
|
||||
"description": "Saves all recent video clips to local directory with file pattern \"%Y%m%d_%H%M%S_[camera name].mp4\".",
|
||||
"fields": {
|
||||
"file_path": {
|
||||
"name": "Output directory",
|
||||
|
||||
@@ -65,11 +65,13 @@ class BMWLock(BMWBaseEntity, LockEntity):
|
||||
try:
|
||||
await self.vehicle.remote_services.trigger_remote_door_lock()
|
||||
except MyBMWAPIError as ex:
|
||||
self._attr_is_locked = False
|
||||
# Set the state to unknown if the command fails
|
||||
self._attr_is_locked = None
|
||||
self.async_write_ha_state()
|
||||
raise HomeAssistantError(ex) from ex
|
||||
|
||||
self.coordinator.async_update_listeners()
|
||||
finally:
|
||||
# Always update the listeners to get the latest state
|
||||
self.coordinator.async_update_listeners()
|
||||
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Unlock the car."""
|
||||
@@ -83,11 +85,13 @@ class BMWLock(BMWBaseEntity, LockEntity):
|
||||
try:
|
||||
await self.vehicle.remote_services.trigger_remote_door_unlock()
|
||||
except MyBMWAPIError as ex:
|
||||
self._attr_is_locked = True
|
||||
# Set the state to unknown if the command fails
|
||||
self._attr_is_locked = None
|
||||
self.async_write_ha_state()
|
||||
raise HomeAssistantError(ex) from ex
|
||||
|
||||
self.coordinator.async_update_listeners()
|
||||
finally:
|
||||
# Always update the listeners to get the latest state
|
||||
self.coordinator.async_update_listeners()
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
|
||||
@@ -6,9 +6,8 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import datetime
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from bimmer_connected.models import ValueWithUnit
|
||||
from bimmer_connected.models import StrEnum, ValueWithUnit
|
||||
from bimmer_connected.vehicle import MyBMWVehicle
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -18,14 +17,19 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import LENGTH, PERCENTAGE, VOLUME, UnitOfElectricCurrent
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
STATE_UNKNOWN,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfLength,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import BMWBaseEntity
|
||||
from .const import CLIMATE_ACTIVITY_STATE, DOMAIN, UNIT_MAP
|
||||
from .const import CLIMATE_ACTIVITY_STATE, DOMAIN
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -36,34 +40,18 @@ class BMWSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes BMW sensor entity."""
|
||||
|
||||
key_class: str | None = None
|
||||
unit_type: str | None = None
|
||||
value: Callable = lambda x, y: x
|
||||
is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled
|
||||
|
||||
|
||||
def convert_and_round(
|
||||
state: ValueWithUnit,
|
||||
converter: Callable[[float | None, str], float],
|
||||
precision: int,
|
||||
) -> float | None:
|
||||
"""Safely convert and round a value from ValueWithUnit."""
|
||||
if state.value and state.unit:
|
||||
return round(
|
||||
converter(state.value, UNIT_MAP.get(state.unit, state.unit)), precision
|
||||
)
|
||||
if state.value:
|
||||
return state.value
|
||||
return None
|
||||
|
||||
|
||||
SENSOR_TYPES: list[BMWSensorEntityDescription] = [
|
||||
# --- Generic ---
|
||||
BMWSensorEntityDescription(
|
||||
key="ac_current_limit",
|
||||
translation_key="ac_current_limit",
|
||||
key_class="charging_profile",
|
||||
unit_type=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
entity_registry_enabled_default=False,
|
||||
suggested_display_precision=0,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
@@ -85,74 +73,81 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
|
||||
key="charging_status",
|
||||
translation_key="charging_status",
|
||||
key_class="fuel_and_battery",
|
||||
value=lambda x, y: x.value,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="charging_target",
|
||||
translation_key="charging_target",
|
||||
key_class="fuel_and_battery",
|
||||
unit_type=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
suggested_display_precision=0,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="remaining_battery_percent",
|
||||
translation_key="remaining_battery_percent",
|
||||
key_class="fuel_and_battery",
|
||||
unit_type=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
|
||||
),
|
||||
# --- Specific ---
|
||||
BMWSensorEntityDescription(
|
||||
key="mileage",
|
||||
translation_key="mileage",
|
||||
unit_type=LENGTH,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="remaining_range_total",
|
||||
translation_key="remaining_range_total",
|
||||
key_class="fuel_and_battery",
|
||||
unit_type=LENGTH,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="remaining_range_electric",
|
||||
translation_key="remaining_range_electric",
|
||||
key_class="fuel_and_battery",
|
||||
unit_type=LENGTH,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="remaining_range_fuel",
|
||||
translation_key="remaining_range_fuel",
|
||||
key_class="fuel_and_battery",
|
||||
unit_type=LENGTH,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="remaining_fuel",
|
||||
translation_key="remaining_fuel",
|
||||
key_class="fuel_and_battery",
|
||||
unit_type=VOLUME,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2),
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
native_unit_of_measurement=UnitOfVolume.LITERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="remaining_fuel_percent",
|
||||
translation_key="remaining_fuel_percent",
|
||||
key_class="fuel_and_battery",
|
||||
unit_type=PERCENTAGE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
@@ -161,7 +156,6 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
|
||||
key_class="climate",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=CLIMATE_ACTIVITY_STATE,
|
||||
value=lambda x, _: x.lower() if x != "UNKNOWN" else None,
|
||||
is_available=lambda v: v.is_remote_climate_stop_enabled,
|
||||
),
|
||||
]
|
||||
@@ -201,13 +195,6 @@ class BMWSensor(BMWBaseEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{vehicle.vin}-{description.key}"
|
||||
|
||||
# Set the correct unit of measurement based on the unit_type
|
||||
if description.unit_type:
|
||||
self._attr_native_unit_of_measurement = (
|
||||
coordinator.hass.config.units.as_dict().get(description.unit_type)
|
||||
or description.unit_type
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
@@ -225,8 +212,18 @@ class BMWSensor(BMWBaseEntity, SensorEntity):
|
||||
# For datetime without tzinfo, we assume it to be the same timezone as the HA instance
|
||||
if isinstance(state, datetime.datetime) and state.tzinfo is None:
|
||||
state = state.replace(tzinfo=dt_util.get_default_time_zone())
|
||||
# For enum types, we only want the value
|
||||
elif isinstance(state, ValueWithUnit):
|
||||
state = state.value
|
||||
# Get lowercase values from StrEnum
|
||||
elif isinstance(state, StrEnum):
|
||||
state = state.value.lower()
|
||||
if state == STATE_UNKNOWN:
|
||||
state = None
|
||||
|
||||
self._attr_native_value = cast(
|
||||
StateType, self.entity_description.value(state, self.hass)
|
||||
)
|
||||
# special handling for charging_status to avoid a breaking change
|
||||
if self.entity_description.key == "charging_status" and state:
|
||||
state = state.upper()
|
||||
|
||||
self._attr_native_value = state
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@@ -3,16 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from brother import Brother, SnmpError
|
||||
from pysnmp.hlapi.asyncio.cmdgen import lcd
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.components.snmp import async_get_snmp_engine
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_TYPE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN, SNMP_ENGINE
|
||||
from .coordinator import BrotherDataUpdateCoordinator
|
||||
from .utils import get_snmp_engine
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
@@ -24,7 +22,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> b
|
||||
host = entry.data[CONF_HOST]
|
||||
printer_type = entry.data[CONF_TYPE]
|
||||
|
||||
snmp_engine = get_snmp_engine(hass)
|
||||
snmp_engine = await async_get_snmp_engine(hass)
|
||||
try:
|
||||
brother = await Brother.create(
|
||||
host, printer_type=printer_type, snmp_engine=snmp_engine
|
||||
@@ -44,16 +42,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> b
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
loaded_entries = [
|
||||
entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.state == ConfigEntryState.LOADED
|
||||
]
|
||||
# We only want to remove the SNMP engine when unloading the last config entry
|
||||
if unload_ok and len(loaded_entries) == 1:
|
||||
lcd.unconfigure(hass.data[SNMP_ENGINE], None)
|
||||
hass.data.pop(SNMP_ENGINE)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -8,13 +8,13 @@ from brother import Brother, SnmpError, UnsupportedModelError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.snmp import async_get_snmp_engine
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_TYPE
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util.network import is_host_valid
|
||||
|
||||
from .const import DOMAIN, PRINTER_TYPES
|
||||
from .utils import get_snmp_engine
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -45,7 +45,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not is_host_valid(user_input[CONF_HOST]):
|
||||
raise InvalidHost
|
||||
|
||||
snmp_engine = get_snmp_engine(self.hass)
|
||||
snmp_engine = await async_get_snmp_engine(self.hass)
|
||||
|
||||
brother = await Brother.create(
|
||||
user_input[CONF_HOST], snmp_engine=snmp_engine
|
||||
@@ -79,7 +79,7 @@ class BrotherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# Do not probe the device if the host is already configured
|
||||
self._async_abort_entries_match({CONF_HOST: self.host})
|
||||
|
||||
snmp_engine = get_snmp_engine(self.hass)
|
||||
snmp_engine = await async_get_snmp_engine(self.hass)
|
||||
model = discovery_info.properties.get("product")
|
||||
|
||||
try:
|
||||
|
||||
@@ -9,6 +9,4 @@ DOMAIN: Final = "brother"
|
||||
|
||||
PRINTER_TYPES: Final = ["laser", "ink"]
|
||||
|
||||
SNMP_ENGINE: Final = "snmp_engine"
|
||||
|
||||
UPDATE_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"domain": "brother",
|
||||
"name": "Brother Printer",
|
||||
"after_dependencies": ["snmp"],
|
||||
"codeowners": ["@bieniu"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/brother",
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"""Brother helpers functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pysnmp.hlapi.asyncio as hlapi
|
||||
from pysnmp.hlapi.asyncio.cmdgen import lcd
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import singleton
|
||||
|
||||
from .const import SNMP_ENGINE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@singleton.singleton(SNMP_ENGINE)
|
||||
def get_snmp_engine(hass: HomeAssistant) -> hlapi.SnmpEngine:
|
||||
"""Get SNMP engine."""
|
||||
_LOGGER.debug("Creating SNMP engine")
|
||||
snmp_engine = hlapi.SnmpEngine()
|
||||
|
||||
@callback
|
||||
def shutdown_listener(ev: Event) -> None:
|
||||
if hass.data.get(SNMP_ENGINE):
|
||||
_LOGGER.debug("Unconfiguring SNMP engine")
|
||||
lcd.unconfigure(hass.data[SNMP_ENGINE], None)
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener)
|
||||
|
||||
return snmp_engine
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/buienradar",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["buienradar", "vincenty"],
|
||||
"requirements": ["buienradar==1.0.5"]
|
||||
"requirements": ["buienradar==1.0.6"]
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
HVACMode.HEAT,
|
||||
HVACMode.COOL,
|
||||
HVACMode.DRY,
|
||||
HVACMode.FAN_ONLY,
|
||||
HVACMode.AUTO,
|
||||
]
|
||||
_attr_fan_modes = [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH]
|
||||
|
||||
@@ -4,11 +4,10 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from . import DOMAIN, ClimateEntity
|
||||
from . import DOMAIN
|
||||
|
||||
INTENT_GET_TEMPERATURE = "HassClimateGetTemperature"
|
||||
|
||||
@@ -23,7 +22,10 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
|
||||
intent_type = INTENT_GET_TEMPERATURE
|
||||
description = "Gets the current temperature of a climate device or entity"
|
||||
slot_schema = {vol.Optional("area"): str, vol.Optional("name"): str}
|
||||
slot_schema = {
|
||||
vol.Optional("area"): intent.non_empty_string,
|
||||
vol.Optional("name"): intent.non_empty_string,
|
||||
}
|
||||
platforms = {DOMAIN}
|
||||
|
||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
@@ -31,74 +33,24 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
hass = intent_obj.hass
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
|
||||
component: EntityComponent[ClimateEntity] = hass.data[DOMAIN]
|
||||
entities: list[ClimateEntity] = list(component.entities)
|
||||
climate_entity: ClimateEntity | None = None
|
||||
climate_state: State | None = None
|
||||
name: str | None = None
|
||||
if "name" in slots:
|
||||
name = slots["name"]["value"]
|
||||
|
||||
if not entities:
|
||||
raise intent.IntentHandleError("No climate entities")
|
||||
area: str | None = None
|
||||
if "area" in slots:
|
||||
area = slots["area"]["value"]
|
||||
|
||||
name_slot = slots.get("name", {})
|
||||
entity_name: str | None = name_slot.get("value")
|
||||
entity_text: str | None = name_slot.get("text")
|
||||
|
||||
area_slot = slots.get("area", {})
|
||||
area_id = area_slot.get("value")
|
||||
|
||||
if area_id:
|
||||
# Filter by area and optionally name
|
||||
area_name = area_slot.get("text")
|
||||
|
||||
for maybe_climate in intent.async_match_states(
|
||||
hass, name=entity_name, area_name=area_id, domains=[DOMAIN]
|
||||
):
|
||||
climate_state = maybe_climate
|
||||
break
|
||||
|
||||
if climate_state is None:
|
||||
raise intent.NoStatesMatchedError(
|
||||
reason=intent.MatchFailedReason.AREA,
|
||||
name=entity_text or entity_name,
|
||||
area=area_name or area_id,
|
||||
floor=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
|
||||
climate_entity = component.get_entity(climate_state.entity_id)
|
||||
elif entity_name:
|
||||
# Filter by name
|
||||
for maybe_climate in intent.async_match_states(
|
||||
hass, name=entity_name, domains=[DOMAIN]
|
||||
):
|
||||
climate_state = maybe_climate
|
||||
break
|
||||
|
||||
if climate_state is None:
|
||||
raise intent.NoStatesMatchedError(
|
||||
reason=intent.MatchFailedReason.NAME,
|
||||
name=entity_name,
|
||||
area=None,
|
||||
floor=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
|
||||
climate_entity = component.get_entity(climate_state.entity_id)
|
||||
else:
|
||||
# First entity
|
||||
climate_entity = entities[0]
|
||||
climate_state = hass.states.get(climate_entity.entity_id)
|
||||
|
||||
assert climate_entity is not None
|
||||
|
||||
if climate_state is None:
|
||||
raise intent.IntentHandleError(f"No state for {climate_entity.name}")
|
||||
|
||||
assert climate_state is not None
|
||||
match_constraints = intent.MatchTargetsConstraints(
|
||||
name=name, area_name=area, domains=[DOMAIN], assistant=intent_obj.assistant
|
||||
)
|
||||
match_result = intent.async_match_targets(hass, match_constraints)
|
||||
if not match_result.is_match:
|
||||
raise intent.MatchFailedError(
|
||||
result=match_result, constraints=match_constraints
|
||||
)
|
||||
|
||||
response = intent_obj.create_response()
|
||||
response.response_type = intent.IntentResponseType.QUERY_ANSWER
|
||||
response.async_set_states(matched_states=[climate_state])
|
||||
response.async_set_states(matched_states=match_result.states)
|
||||
return response
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["hass_nabucasa"],
|
||||
"requirements": ["hass-nabucasa==0.81.0"]
|
||||
"requirements": ["hass-nabucasa==0.81.1"]
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
|
||||
|
||||
self._attr_name = name
|
||||
self._code = code
|
||||
self._alarm_control_panel_option_default_code = code
|
||||
self._mode = mode
|
||||
self._url = url
|
||||
self._alarm = concord232_client.Client(self._url)
|
||||
|
||||
@@ -120,7 +120,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
director_all_items = json.loads(director_all_items)
|
||||
entry_data[CONF_DIRECTOR_ALL_ITEMS] = director_all_items
|
||||
|
||||
entry_data[CONF_UI_CONFIGURATION] = json.loads(await director.getUiConfiguration())
|
||||
# Check if OS version is 3 or higher to get UI configuration
|
||||
entry_data[CONF_UI_CONFIGURATION] = None
|
||||
if int(entry_data[CONF_DIRECTOR_SW_VERSION].split(".")[0]) >= 3:
|
||||
entry_data[CONF_UI_CONFIGURATION] = json.loads(
|
||||
await director.getUiConfiguration()
|
||||
)
|
||||
|
||||
# Load options from config entry
|
||||
entry_data[CONF_SCAN_INTERVAL] = entry.options.get(
|
||||
|
||||
@@ -81,11 +81,18 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up Control4 rooms from a config entry."""
|
||||
entry_data = hass.data[DOMAIN][entry.entry_id]
|
||||
ui_config = entry_data[CONF_UI_CONFIGURATION]
|
||||
|
||||
# OS 2 will not have a ui_configuration
|
||||
if not ui_config:
|
||||
_LOGGER.debug("No UI Configuration found for Control4")
|
||||
return
|
||||
|
||||
all_rooms = await get_rooms(hass, entry)
|
||||
if not all_rooms:
|
||||
return
|
||||
|
||||
entry_data = hass.data[DOMAIN][entry.entry_id]
|
||||
scan_interval = entry_data[CONF_SCAN_INTERVAL]
|
||||
_LOGGER.debug("Scan interval = %s", scan_interval)
|
||||
|
||||
@@ -119,8 +126,6 @@ async def async_setup_entry(
|
||||
if "parentId" in item and k > 1
|
||||
}
|
||||
|
||||
ui_config = entry_data[CONF_UI_CONFIGURATION]
|
||||
|
||||
entity_list = []
|
||||
for room in all_rooms:
|
||||
room_id = room["id"]
|
||||
|
||||
@@ -429,8 +429,15 @@ class DefaultAgent(ConversationEntity):
|
||||
intent_context=intent_context,
|
||||
language=language,
|
||||
):
|
||||
if ("name" in result.entities) and (
|
||||
not result.entities["name"].is_wildcard
|
||||
# Prioritize results with a "name" slot, but still prefer ones with
|
||||
# more literal text matched.
|
||||
if (
|
||||
("name" in result.entities)
|
||||
and (not result.entities["name"].is_wildcard)
|
||||
and (
|
||||
(name_result is None)
|
||||
or (result.text_chunks_matched > name_result.text_chunks_matched)
|
||||
)
|
||||
):
|
||||
name_result = result
|
||||
|
||||
@@ -871,7 +878,7 @@ class DefaultAgent(ConversationEntity):
|
||||
if device_area is None:
|
||||
return None
|
||||
|
||||
return {"area": {"value": device_area.id, "text": device_area.name}}
|
||||
return {"area": {"value": device_area.name, "text": device_area.name}}
|
||||
|
||||
def _get_error_text(
|
||||
self,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.5.28"]
|
||||
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.5"]
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None:
|
||||
DOMAIN,
|
||||
SERVICE_OPEN_COVER,
|
||||
"Opened {}",
|
||||
description="Opens a cover",
|
||||
platforms={DOMAIN},
|
||||
),
|
||||
)
|
||||
@@ -29,6 +30,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None:
|
||||
DOMAIN,
|
||||
SERVICE_CLOSE_COVER,
|
||||
"Closed {}",
|
||||
description="Closes a cover",
|
||||
platforms={DOMAIN},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
DOMAIN = "discovergy"
|
||||
MANUFACTURER = "Discovergy"
|
||||
MANUFACTURER = "inexogy"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "discovergy",
|
||||
"name": "Discovergy",
|
||||
"name": "inexogy",
|
||||
"codeowners": ["@jpbede"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/discovergy",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"api_endpoint_reachable": "Discovergy API endpoint reachable"
|
||||
"api_endpoint_reachable": "inexogy API endpoint reachable"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -149,7 +149,7 @@ class DlnaDmrFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# case the device doesn't have a static and unique UDN (breaking the
|
||||
# UPnP spec).
|
||||
for entry in self._async_current_entries(include_ignore=True):
|
||||
if self._location == entry.data[CONF_URL]:
|
||||
if self._location == entry.data.get(CONF_URL):
|
||||
return self.async_abort(reason="already_configured")
|
||||
if self._mac and self._mac == entry.data.get(CONF_MAC):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
@@ -43,7 +43,9 @@ class EcobeeNotificationService(BaseNotificationService):
|
||||
|
||||
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message and raise issue."""
|
||||
migrate_notify_issue(self.hass, DOMAIN, "Ecobee", "2024.11.0")
|
||||
migrate_notify_issue(
|
||||
self.hass, DOMAIN, "Ecobee", "2024.11.0", service_name=self._service_name
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.send_message, message, **kwargs)
|
||||
)
|
||||
|
||||
@@ -67,6 +67,7 @@ class EgardiaAlarm(AlarmControlPanelEntity):
|
||||
"""Representation of a Egardia alarm."""
|
||||
|
||||
_attr_state: str | None
|
||||
_attr_code_arm_required = False
|
||||
_attr_supported_features = (
|
||||
AlarmControlPanelEntityFeature.ARM_HOME
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/electrasmart",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pyElectra==1.2.0"]
|
||||
"requirements": ["pyElectra==1.2.1"]
|
||||
}
|
||||
|
||||
@@ -59,7 +59,15 @@ class ElgatoLight(ElgatoEntity, LightEntity):
|
||||
self._attr_unique_id = coordinator.data.info.serial_number
|
||||
|
||||
# Elgato Light supporting color, have a different temperature range
|
||||
if self.coordinator.data.settings.power_on_hue is not None:
|
||||
if (
|
||||
self.coordinator.data.info.product_name
|
||||
in (
|
||||
"Elgato Light Strip",
|
||||
"Elgato Light Strip Pro",
|
||||
)
|
||||
or self.coordinator.data.settings.power_on_hue
|
||||
or self.coordinator.data.state.hue is not None
|
||||
):
|
||||
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
|
||||
self._attr_min_mireds = 153
|
||||
self._attr_max_mireds = 285
|
||||
|
||||
@@ -141,10 +141,10 @@ class Enigma2Device(MediaPlayerEntity):
|
||||
self._device: OpenWebIfDevice = device
|
||||
self._entry = entry
|
||||
|
||||
self._attr_unique_id = device.mac_address
|
||||
self._attr_unique_id = device.mac_address or entry.entry_id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.mac_address)},
|
||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||
manufacturer=about["info"]["brand"],
|
||||
model=about["info"]["model"],
|
||||
configuration_url=device.base,
|
||||
|
||||
@@ -116,8 +116,9 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
):
|
||||
"""Initialize the alarm panel."""
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
self._panic_type = panic_type
|
||||
self._alarm_control_panel_option_default_code = code
|
||||
self._attr_code_format = CodeFormat.NUMBER
|
||||
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
super().__init__(alarm_name, info, controller)
|
||||
@@ -141,13 +142,6 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def code_format(self) -> CodeFormat | None:
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
return CodeFormat.NUMBER
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Return the state of the device."""
|
||||
@@ -169,34 +163,15 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number)
|
||||
else:
|
||||
self.hass.data[DATA_EVL].disarm_partition(
|
||||
str(self._code), self._partition_number
|
||||
)
|
||||
self.hass.data[DATA_EVL].disarm_partition(code, self._partition_number)
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
str(code), self._partition_number
|
||||
)
|
||||
else:
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
str(self._code), self._partition_number
|
||||
)
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(code, self._partition_number)
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
str(code), self._partition_number
|
||||
)
|
||||
else:
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
str(self._code), self._partition_number
|
||||
)
|
||||
self.hass.data[DATA_EVL].arm_away_partition(code, self._partition_number)
|
||||
|
||||
async def async_alarm_trigger(self, code: str | None = None) -> None:
|
||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||
@@ -204,9 +179,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
self.hass.data[DATA_EVL].arm_night_partition(
|
||||
str(code) if code else str(self._code), self._partition_number
|
||||
)
|
||||
self.hass.data[DATA_EVL].arm_night_partition(code, self._partition_number)
|
||||
|
||||
@callback
|
||||
def async_alarm_keypress(self, keypress=None):
|
||||
|
||||
@@ -6,7 +6,7 @@ Such systems include evohome, Round Thermostat, and others.
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
import re
|
||||
@@ -452,7 +452,7 @@ class EvoBroker:
|
||||
|
||||
self.config = client.installation_info[loc_idx][GWS][0][TCS][0]
|
||||
self.tcs: evo.ControlSystem = self._location._gateways[0]._control_systems[0] # noqa: SLF001
|
||||
self.tcs_utc_offset = timedelta(minutes=self._location.timeZone[UTC_OFFSET])
|
||||
self.loc_utc_offset = timedelta(minutes=self._location.timeZone[UTC_OFFSET])
|
||||
self.temps: dict[str, float | None] = {}
|
||||
|
||||
async def save_auth_tokens(self) -> None:
|
||||
@@ -685,7 +685,8 @@ class EvoChild(EvoDevice):
|
||||
if not (schedule := self._schedule.get("DailySchedules")):
|
||||
return {} # no scheduled setpoints when {'DailySchedules': []}
|
||||
|
||||
day_time = dt_util.now()
|
||||
# get dt in the same TZ as the TCS location, so we can compare schedule times
|
||||
day_time = dt_util.now().astimezone(timezone(self._evo_broker.loc_utc_offset))
|
||||
day_of_week = day_time.weekday() # for evohome, 0 is Monday
|
||||
time_of_day = day_time.strftime("%H:%M:%S")
|
||||
|
||||
@@ -699,7 +700,7 @@ class EvoChild(EvoDevice):
|
||||
else:
|
||||
break
|
||||
|
||||
# Did the current SP start yesterday? Does the next start SP tomorrow?
|
||||
# Did this setpoint start yesterday? Does the next setpoint start tomorrow?
|
||||
this_sp_day = -1 if sp_idx == -1 else 0
|
||||
next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0
|
||||
|
||||
@@ -716,7 +717,7 @@ class EvoChild(EvoDevice):
|
||||
)
|
||||
assert switchpoint_time_of_day is not None # mypy check
|
||||
dt_aware = _dt_evo_to_aware(
|
||||
switchpoint_time_of_day, self._evo_broker.tcs_utc_offset
|
||||
switchpoint_time_of_day, self._evo_broker.loc_utc_offset
|
||||
)
|
||||
|
||||
self._setpoints[f"{key}_sp_from"] = dt_aware.isoformat()
|
||||
@@ -740,16 +741,18 @@ class EvoChild(EvoDevice):
|
||||
assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check
|
||||
|
||||
try:
|
||||
self._schedule = await self._evo_broker.call_client_api( # type: ignore[assignment]
|
||||
schedule = await self._evo_broker.call_client_api(
|
||||
self._evo_device.get_schedule(), update_state=False
|
||||
)
|
||||
except evo.InvalidSchedule as err:
|
||||
_LOGGER.warning(
|
||||
"%s: Unable to retrieve the schedule: %s",
|
||||
"%s: Unable to retrieve a valid schedule: %s",
|
||||
self._evo_device,
|
||||
err,
|
||||
)
|
||||
self._schedule = {}
|
||||
else:
|
||||
self._schedule = schedule or {}
|
||||
|
||||
_LOGGER.debug("Schedule['%s'] = %s", self.name, self._schedule)
|
||||
|
||||
|
||||
@@ -69,7 +69,9 @@ class FileNotificationService(BaseNotificationService):
|
||||
"""Send a message to a file."""
|
||||
# The use of the legacy notify service was deprecated with HA Core 2024.6.0
|
||||
# and will be removed with HA Core 2024.12
|
||||
migrate_notify_issue(self.hass, DOMAIN, "File", "2024.12.0")
|
||||
migrate_notify_issue(
|
||||
self.hass, DOMAIN, "File", "2024.12.0", service_name=self._service_name
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.send_message, message, **kwargs)
|
||||
)
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240530.0"]
|
||||
"requirements": ["home-assistant-frontend==20240610.1"]
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [
|
||||
FytaSensorEntityDescription(
|
||||
key="light",
|
||||
translation_key="light",
|
||||
native_unit_of_measurement="mol/d",
|
||||
native_unit_of_measurement="μmol/s⋅m²",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
FytaSensorEntityDescription(
|
||||
|
||||
@@ -13,5 +13,6 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["gardena-bluetooth==1.4.1"]
|
||||
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
|
||||
"requirements": ["gardena-bluetooth==1.4.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/glances",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["glances_api"],
|
||||
"requirements": ["glances-api==0.7.0"]
|
||||
"requirements": ["glances-api==0.8.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/goodwe",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["goodwe"],
|
||||
"requirements": ["goodwe==0.3.5"]
|
||||
"requirements": ["goodwe==0.3.6"]
|
||||
}
|
||||
|
||||
@@ -1586,6 +1586,17 @@ class ArmDisArmTrait(_Trait):
|
||||
if features & required_feature != 0
|
||||
]
|
||||
|
||||
def _default_arm_state(self):
|
||||
states = self._supported_states()
|
||||
|
||||
if STATE_ALARM_TRIGGERED in states:
|
||||
states.remove(STATE_ALARM_TRIGGERED)
|
||||
|
||||
if len(states) != 1:
|
||||
raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing")
|
||||
|
||||
return states[0]
|
||||
|
||||
def sync_attributes(self):
|
||||
"""Return ArmDisarm attributes for a sync request."""
|
||||
response = {}
|
||||
@@ -1609,10 +1620,13 @@ class ArmDisArmTrait(_Trait):
|
||||
def query_attributes(self):
|
||||
"""Return ArmDisarm query attributes."""
|
||||
armed_state = self.state.attributes.get("next_state", self.state.state)
|
||||
response = {"isArmed": armed_state in self.state_to_service}
|
||||
if response["isArmed"]:
|
||||
response.update({"currentArmLevel": armed_state})
|
||||
return response
|
||||
|
||||
if armed_state in self.state_to_service:
|
||||
return {"isArmed": True, "currentArmLevel": armed_state}
|
||||
return {
|
||||
"isArmed": False,
|
||||
"currentArmLevel": self._default_arm_state(),
|
||||
}
|
||||
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute an ArmDisarm command."""
|
||||
@@ -1620,15 +1634,7 @@ class ArmDisArmTrait(_Trait):
|
||||
# If no arm level given, we can only arm it if there is
|
||||
# only one supported arm type. We never default to triggered.
|
||||
if not (arm_level := params.get("armLevel")):
|
||||
states = self._supported_states()
|
||||
|
||||
if STATE_ALARM_TRIGGERED in states:
|
||||
states.remove(STATE_ALARM_TRIGGERED)
|
||||
|
||||
if len(states) != 1:
|
||||
raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing")
|
||||
|
||||
arm_level = states[0]
|
||||
arm_level = self._default_arm_state()
|
||||
|
||||
if self.state.state == arm_level:
|
||||
raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed")
|
||||
|
||||
@@ -165,7 +165,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent):
|
||||
await session.async_ensure_token_valid()
|
||||
self.assistant = None
|
||||
if not self.assistant or user_input.language != self.language:
|
||||
credentials = Credentials(session.token[CONF_ACCESS_TOKEN])
|
||||
credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
|
||||
self.language = user_input.language
|
||||
self.assistant = TextAssistant(credentials, self.language)
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ async def async_send_text_commands(
|
||||
entry.async_start_reauth(hass)
|
||||
raise
|
||||
|
||||
credentials = Credentials(session.token[CONF_ACCESS_TOKEN])
|
||||
credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
|
||||
language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass))
|
||||
with TextAssistant(
|
||||
credentials, language_code, audio_out=bool(media_players)
|
||||
|
||||
@@ -66,13 +66,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
}
|
||||
)
|
||||
|
||||
model_name = "gemini-pro-vision" if image_filenames else "gemini-pro"
|
||||
model = genai.GenerativeModel(model_name=model_name)
|
||||
model = genai.GenerativeModel(model_name=RECOMMENDED_CHAT_MODEL)
|
||||
|
||||
try:
|
||||
response = await model.generate_content_async(prompt_parts)
|
||||
except (
|
||||
ClientError,
|
||||
GoogleAPICallError,
|
||||
ValueError,
|
||||
genai_types.BlockedPromptException,
|
||||
genai_types.StopCandidateException,
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import codecs
|
||||
from typing import Any, Literal
|
||||
|
||||
import google.ai.generativelanguage as glm
|
||||
from google.api_core.exceptions import GoogleAPICallError
|
||||
import google.generativeai as genai
|
||||
from google.generativeai import protos
|
||||
import google.generativeai.types as genai_types
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
import voluptuous as vol
|
||||
from voluptuous_openapi import convert
|
||||
|
||||
@@ -92,7 +94,7 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]:
|
||||
|
||||
parameters = _format_schema(convert(tool.parameters))
|
||||
|
||||
return glm.Tool(
|
||||
return protos.Tool(
|
||||
{
|
||||
"function_declarations": [
|
||||
{
|
||||
@@ -105,6 +107,17 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]:
|
||||
)
|
||||
|
||||
|
||||
def _escape_decode(value: Any) -> Any:
|
||||
"""Recursively call codecs.escape_decode on all values."""
|
||||
if isinstance(value, str):
|
||||
return codecs.escape_decode(bytes(value, "utf-8"))[0].decode("utf-8") # type: ignore[attr-defined]
|
||||
if isinstance(value, list):
|
||||
return [_escape_decode(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {k: _escape_decode(v) for k, v in value.items()}
|
||||
return value
|
||||
|
||||
|
||||
class GoogleGenerativeAIConversationEntity(
|
||||
conversation.ConversationEntity, conversation.AbstractConversationAgent
|
||||
):
|
||||
@@ -151,20 +164,22 @@ class GoogleGenerativeAIConversationEntity(
|
||||
intent_response = intent.IntentResponse(language=user_input.language)
|
||||
llm_api: llm.APIInstance | None = None
|
||||
tools: list[dict[str, Any]] | None = None
|
||||
user_name: str | None = None
|
||||
llm_context = llm.LLMContext(
|
||||
platform=DOMAIN,
|
||||
context=user_input.context,
|
||||
user_prompt=user_input.text,
|
||||
language=user_input.language,
|
||||
assistant=conversation.DOMAIN,
|
||||
device_id=user_input.device_id,
|
||||
)
|
||||
|
||||
if self.entry.options.get(CONF_LLM_HASS_API):
|
||||
try:
|
||||
llm_api = await llm.async_get_api(
|
||||
self.hass,
|
||||
self.entry.options[CONF_LLM_HASS_API],
|
||||
llm.ToolContext(
|
||||
platform=DOMAIN,
|
||||
context=user_input.context,
|
||||
user_prompt=user_input.text,
|
||||
language=user_input.language,
|
||||
assistant=conversation.DOMAIN,
|
||||
device_id=user_input.device_id,
|
||||
),
|
||||
llm_context,
|
||||
)
|
||||
except HomeAssistantError as err:
|
||||
LOGGER.error("Error getting LLM API: %s", err)
|
||||
@@ -211,7 +226,16 @@ class GoogleGenerativeAIConversationEntity(
|
||||
messages = self.history[conversation_id]
|
||||
else:
|
||||
conversation_id = ulid.ulid_now()
|
||||
messages = [{}, {}]
|
||||
messages = [{}, {"role": "model", "parts": "Ok"}]
|
||||
|
||||
if (
|
||||
user_input.context
|
||||
and user_input.context.user_id
|
||||
and (
|
||||
user := await self.hass.auth.async_get_user(user_input.context.user_id)
|
||||
)
|
||||
):
|
||||
user_name = user.name
|
||||
|
||||
try:
|
||||
if llm_api:
|
||||
@@ -222,13 +246,16 @@ class GoogleGenerativeAIConversationEntity(
|
||||
prompt = "\n".join(
|
||||
(
|
||||
template.Template(
|
||||
self.entry.options.get(
|
||||
llm.BASE_PROMPT
|
||||
+ self.entry.options.get(
|
||||
CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT
|
||||
),
|
||||
self.hass,
|
||||
).async_render(
|
||||
{
|
||||
"ha_name": self.hass.config.location_name,
|
||||
"user_name": user_name,
|
||||
"llm_context": llm_context,
|
||||
},
|
||||
parse_result=False,
|
||||
),
|
||||
@@ -246,8 +273,11 @@ class GoogleGenerativeAIConversationEntity(
|
||||
response=intent_response, conversation_id=conversation_id
|
||||
)
|
||||
|
||||
messages[0] = {"role": "user", "parts": prompt}
|
||||
messages[1] = {"role": "model", "parts": "Ok"}
|
||||
# Make a copy, because we attach it to the trace event.
|
||||
messages = [
|
||||
{"role": "user", "parts": prompt},
|
||||
*messages[1:],
|
||||
]
|
||||
|
||||
LOGGER.debug("Input: '%s' with history: %s", user_input.text, messages)
|
||||
trace.async_conversation_trace_append(
|
||||
@@ -295,21 +325,19 @@ class GoogleGenerativeAIConversationEntity(
|
||||
response=intent_response, conversation_id=conversation_id
|
||||
)
|
||||
self.history[conversation_id] = chat.history
|
||||
tool_calls = [
|
||||
function_calls = [
|
||||
part.function_call for part in chat_response.parts if part.function_call
|
||||
]
|
||||
if not tool_calls or not llm_api:
|
||||
if not function_calls or not llm_api:
|
||||
break
|
||||
|
||||
tool_responses = []
|
||||
for tool_call in tool_calls:
|
||||
tool_input = llm.ToolInput(
|
||||
tool_name=tool_call.name,
|
||||
tool_args=dict(tool_call.args),
|
||||
)
|
||||
LOGGER.debug(
|
||||
"Tool call: %s(%s)", tool_input.tool_name, tool_input.tool_args
|
||||
)
|
||||
for function_call in function_calls:
|
||||
tool_call = MessageToDict(function_call._pb) # noqa: SLF001
|
||||
tool_name = tool_call["name"]
|
||||
tool_args = _escape_decode(tool_call["args"])
|
||||
LOGGER.debug("Tool call: %s(%s)", tool_name, tool_args)
|
||||
tool_input = llm.ToolInput(tool_name=tool_name, tool_args=tool_args)
|
||||
try:
|
||||
function_response = await llm_api.async_call_tool(tool_input)
|
||||
except (HomeAssistantError, vol.Invalid) as e:
|
||||
@@ -319,16 +347,16 @@ class GoogleGenerativeAIConversationEntity(
|
||||
|
||||
LOGGER.debug("Tool response: %s", function_response)
|
||||
tool_responses.append(
|
||||
glm.Part(
|
||||
function_response=glm.FunctionResponse(
|
||||
name=tool_call.name, response=function_response
|
||||
protos.Part(
|
||||
function_response=protos.FunctionResponse(
|
||||
name=tool_name, response=function_response
|
||||
)
|
||||
)
|
||||
)
|
||||
chat_request = glm.Content(parts=tool_responses)
|
||||
chat_request = protos.Content(parts=tool_responses)
|
||||
|
||||
intent_response.async_set_speech(
|
||||
" ".join([part.text for part in chat_response.parts if part.text])
|
||||
" ".join([part.text.strip() for part in chat_response.parts if part.text])
|
||||
)
|
||||
return conversation.ConversationResult(
|
||||
response=intent_response, conversation_id=conversation_id
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["google-generativeai==0.5.4", "voluptuous-openapi==0.0.4"]
|
||||
"requirements": ["google-generativeai==0.6.0", "voluptuous-openapi==0.0.4"]
|
||||
}
|
||||
|
||||
@@ -93,7 +93,9 @@ async def async_setup_service(hass: HomeAssistant) -> None:
|
||||
|
||||
def _append_to_sheet(call: ServiceCall, entry: ConfigEntry) -> None:
|
||||
"""Run append in the executor."""
|
||||
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]))
|
||||
service = Client(
|
||||
Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
|
||||
)
|
||||
try:
|
||||
sheet = service.open_by_key(entry.unique_id)
|
||||
except RefreshError:
|
||||
|
||||
@@ -61,7 +61,9 @@ class OAuth2FlowHandler(
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Create an entry for the flow, or update existing entry."""
|
||||
service = Client(Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]))
|
||||
service = Client(
|
||||
Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
|
||||
)
|
||||
|
||||
if self.reauth_entry:
|
||||
_LOGGER.debug("service.open_by_key")
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
{
|
||||
"domain": "group",
|
||||
"name": "Group",
|
||||
"after_dependencies": [
|
||||
"alarm_control_panel",
|
||||
"climate",
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"lock",
|
||||
"media_player",
|
||||
"person",
|
||||
"plant",
|
||||
"vacuum",
|
||||
"water_heater"
|
||||
],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/group",
|
||||
|
||||
@@ -36,7 +36,14 @@ from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
EventStateChangedData,
|
||||
HomeAssistant,
|
||||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.entity import (
|
||||
@@ -45,6 +52,7 @@ from homeassistant.helpers.entity import (
|
||||
get_unit_of_measurement,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
@@ -329,6 +337,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
self._native_unit_of_measurement = unit_of_measurement
|
||||
self._valid_units: set[str | None] = set()
|
||||
self._can_convert: bool = False
|
||||
self.calculate_attributes_later: CALLBACK_TYPE | None = None
|
||||
self._attr_name = name
|
||||
if name == DEFAULT_NAME:
|
||||
self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize()
|
||||
@@ -345,13 +354,32 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When added to hass."""
|
||||
for entity_id in self._entity_ids:
|
||||
if self.hass.states.get(entity_id) is None:
|
||||
self.calculate_attributes_later = async_track_state_change_event(
|
||||
self.hass, self._entity_ids, self.calculate_state_attributes
|
||||
)
|
||||
break
|
||||
if not self.calculate_attributes_later:
|
||||
await self.calculate_state_attributes()
|
||||
await super().async_added_to_hass()
|
||||
|
||||
async def calculate_state_attributes(
|
||||
self, event: Event[EventStateChangedData] | None = None
|
||||
) -> None:
|
||||
"""Calculate state attributes."""
|
||||
for entity_id in self._entity_ids:
|
||||
if self.hass.states.get(entity_id) is None:
|
||||
return
|
||||
if self.calculate_attributes_later:
|
||||
self.calculate_attributes_later()
|
||||
self.calculate_attributes_later = None
|
||||
self._attr_state_class = self._calculate_state_class(self._state_class)
|
||||
self._attr_device_class = self._calculate_device_class(self._device_class)
|
||||
self._attr_native_unit_of_measurement = self._calculate_unit_of_measurement(
|
||||
self._native_unit_of_measurement
|
||||
)
|
||||
self._valid_units = self._get_valid_units()
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
def async_update_group_state(self) -> None:
|
||||
|
||||
@@ -267,15 +267,14 @@ class SupervisorIssues:
|
||||
placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference}
|
||||
|
||||
if issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON_URL] = (
|
||||
f"/hassio/addon/{issue.reference}"
|
||||
)
|
||||
addons = get_addons_info(self._hass)
|
||||
if addons and issue.reference in addons:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][
|
||||
"name"
|
||||
]
|
||||
if "url" in addons[issue.reference]:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON_URL] = addons[
|
||||
issue.reference
|
||||
]["url"]
|
||||
else:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity):
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
| AlarmControlPanelEntityFeature.TRIGGER
|
||||
)
|
||||
_attr_code_arm_required = False
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -18,16 +18,10 @@ from homeassistant.util import dt as dt_util
|
||||
from .const import CONF_PROVINCE, DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Holiday Calendar config entry."""
|
||||
country: str = config_entry.data[CONF_COUNTRY]
|
||||
province: str | None = config_entry.data.get(CONF_PROVINCE)
|
||||
language = hass.config.language
|
||||
|
||||
def _get_obj_holidays_and_language(
|
||||
country: str, province: str | None, language: str
|
||||
) -> tuple[HolidayBase, str]:
|
||||
"""Get the object for the requested country and year."""
|
||||
obj_holidays = country_holidays(
|
||||
country,
|
||||
subdiv=province,
|
||||
@@ -58,6 +52,23 @@ async def async_setup_entry(
|
||||
)
|
||||
language = default_language
|
||||
|
||||
return (obj_holidays, language)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Holiday Calendar config entry."""
|
||||
country: str = config_entry.data[CONF_COUNTRY]
|
||||
province: str | None = config_entry.data.get(CONF_PROVINCE)
|
||||
language = hass.config.language
|
||||
|
||||
obj_holidays, language = await hass.async_add_executor_job(
|
||||
_get_obj_holidays_and_language, country, province, language
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
HolidayCalendarEntity(
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.49", "babel==2.13.1"]
|
||||
"requirements": ["holidays==0.50", "babel==2.13.1"]
|
||||
}
|
||||
|
||||
@@ -35,9 +35,8 @@ DEFAULT_EXPOSED_DOMAINS = {
|
||||
"fan",
|
||||
"humidifier",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"scene",
|
||||
"script",
|
||||
"switch",
|
||||
"todo",
|
||||
"vacuum",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"domain": "http",
|
||||
"name": "HTTP",
|
||||
"after_dependencies": ["isal"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/http",
|
||||
"integration_type": "system",
|
||||
|
||||
@@ -35,7 +35,7 @@ class HumidityHandler(intent.IntentHandler):
|
||||
intent_type = INTENT_HUMIDITY
|
||||
description = "Set desired humidity level"
|
||||
slot_schema = {
|
||||
vol.Required("name"): cv.string,
|
||||
vol.Required("name"): intent.non_empty_string,
|
||||
vol.Required("humidity"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
|
||||
}
|
||||
platforms = {DOMAIN}
|
||||
@@ -44,18 +44,19 @@ class HumidityHandler(intent.IntentHandler):
|
||||
"""Handle the hass intent."""
|
||||
hass = intent_obj.hass
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
states = list(
|
||||
intent.async_match_states(
|
||||
hass,
|
||||
name=slots["name"]["value"],
|
||||
states=hass.states.async_all(DOMAIN),
|
||||
)
|
||||
|
||||
match_constraints = intent.MatchTargetsConstraints(
|
||||
name=slots["name"]["value"],
|
||||
domains=[DOMAIN],
|
||||
assistant=intent_obj.assistant,
|
||||
)
|
||||
match_result = intent.async_match_targets(hass, match_constraints)
|
||||
if not match_result.is_match:
|
||||
raise intent.MatchFailedError(
|
||||
result=match_result, constraints=match_constraints
|
||||
)
|
||||
|
||||
if not states:
|
||||
raise intent.IntentHandleError("No entities matched")
|
||||
|
||||
state = states[0]
|
||||
state = match_result.states[0]
|
||||
service_data = {ATTR_ENTITY_ID: state.entity_id}
|
||||
|
||||
humidity = slots["humidity"]["value"]
|
||||
@@ -89,7 +90,7 @@ class SetModeHandler(intent.IntentHandler):
|
||||
intent_type = INTENT_MODE
|
||||
description = "Set humidifier mode"
|
||||
slot_schema = {
|
||||
vol.Required("name"): cv.string,
|
||||
vol.Required("name"): intent.non_empty_string,
|
||||
vol.Required("mode"): cv.string,
|
||||
}
|
||||
platforms = {DOMAIN}
|
||||
@@ -98,18 +99,18 @@ class SetModeHandler(intent.IntentHandler):
|
||||
"""Handle the hass intent."""
|
||||
hass = intent_obj.hass
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
states = list(
|
||||
intent.async_match_states(
|
||||
hass,
|
||||
name=slots["name"]["value"],
|
||||
states=hass.states.async_all(DOMAIN),
|
||||
)
|
||||
match_constraints = intent.MatchTargetsConstraints(
|
||||
name=slots["name"]["value"],
|
||||
domains=[DOMAIN],
|
||||
assistant=intent_obj.assistant,
|
||||
)
|
||||
match_result = intent.async_match_targets(hass, match_constraints)
|
||||
if not match_result.is_match:
|
||||
raise intent.MatchFailedError(
|
||||
result=match_result, constraints=match_constraints
|
||||
)
|
||||
|
||||
if not states:
|
||||
raise intent.IntentHandleError("No entities matched")
|
||||
|
||||
state = states[0]
|
||||
state = match_result.states[0]
|
||||
service_data = {ATTR_ENTITY_ID: state.entity_id}
|
||||
|
||||
intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes")
|
||||
|
||||
@@ -24,13 +24,17 @@ class HydrawiseBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes Hydrawise binary sensor."""
|
||||
|
||||
value_fn: Callable[[HydrawiseBinarySensor], bool | None]
|
||||
always_available: bool = False
|
||||
|
||||
|
||||
CONTROLLER_BINARY_SENSORS: tuple[HydrawiseBinarySensorEntityDescription, ...] = (
|
||||
HydrawiseBinarySensorEntityDescription(
|
||||
key="status",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
value_fn=lambda status_sensor: status_sensor.coordinator.last_update_success,
|
||||
value_fn=lambda status_sensor: status_sensor.coordinator.last_update_success
|
||||
and status_sensor.controller.online,
|
||||
# Connectivtiy sensor is always available
|
||||
always_available=True,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -98,3 +102,10 @@ class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update state attributes."""
|
||||
self._attr_is_on = self.entity_description.value_fn(self)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Set the entity availability."""
|
||||
if self.entity_description.always_available:
|
||||
return True
|
||||
return super().available
|
||||
|
||||
@@ -70,3 +70,8 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]):
|
||||
self.controller = self.coordinator.data.controllers[self.controller.id]
|
||||
self._update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Set the entity availability."""
|
||||
return super().available and self.controller.online
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2024.4.1"]
|
||||
"requirements": ["pydrawise==2024.6.3"]
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ class IAlarmPanel(
|
||||
AlarmControlPanelEntityFeature.ARM_HOME
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
)
|
||||
_attr_code_arm_required = False
|
||||
|
||||
def __init__(self, coordinator: IAlarmDataUpdateCoordinator) -> None:
|
||||
"""Create the entity with a DataUpdateCoordinator."""
|
||||
|
||||
@@ -64,7 +64,7 @@ class IdasenDeskConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
desk = Desk(None, monitor_height=False)
|
||||
try:
|
||||
await desk.connect(discovery_info.device, auto_reconnect=False)
|
||||
await desk.connect(discovery_info.device, retry=False)
|
||||
except AuthFailedError:
|
||||
errors["base"] = "auth_failed"
|
||||
except TimeoutError:
|
||||
|
||||
@@ -195,13 +195,13 @@ class ImapMessage:
|
||||
):
|
||||
message_untyped_text = str(part.get_payload())
|
||||
|
||||
if message_text is not None:
|
||||
if message_text is not None and message_text.strip():
|
||||
return message_text
|
||||
|
||||
if message_html is not None:
|
||||
if message_html:
|
||||
return message_html
|
||||
|
||||
if message_untyped_text is not None:
|
||||
if message_untyped_text:
|
||||
return message_untyped_text
|
||||
|
||||
return str(self.email_message.get_payload())
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["imgw_pib==1.0.1"]
|
||||
"requirements": ["imgw_pib==1.0.5"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Any, TypedDict
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.script import CONF_MODE
|
||||
from homeassistant.const import CONF_TYPE, SERVICE_RELOAD
|
||||
from homeassistant.const import CONF_DESCRIPTION, CONF_TYPE, SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
@@ -24,6 +24,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "intent_script"
|
||||
|
||||
CONF_PLATFORMS = "platforms"
|
||||
CONF_INTENTS = "intents"
|
||||
CONF_SPEECH = "speech"
|
||||
CONF_REPROMPT = "reprompt"
|
||||
@@ -41,6 +42,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: {
|
||||
cv.string: {
|
||||
vol.Optional(CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(CONF_PLATFORMS): vol.All([cv.string], vol.Coerce(set)),
|
||||
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(
|
||||
CONF_ASYNC_ACTION, default=DEFAULT_CONF_ASYNC_ACTION
|
||||
@@ -146,6 +149,8 @@ class ScriptIntentHandler(intent.IntentHandler):
|
||||
"""Initialize the script intent handler."""
|
||||
self.intent_type = intent_type
|
||||
self.config = config
|
||||
self.description = config.get(CONF_DESCRIPTION)
|
||||
self.platforms = config.get(CONF_PLATFORMS)
|
||||
|
||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
"""Handle the intent."""
|
||||
|
||||
@@ -16,11 +16,13 @@ from homeassistant.const import (
|
||||
CONF_TIME_ZONE,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .binary_sensor import BINARY_SENSORS
|
||||
from .const import (
|
||||
CONF_CANDLE_LIGHT_MINUTES,
|
||||
CONF_DIASPORA,
|
||||
@@ -32,6 +34,7 @@ from .const import (
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
from .sensor import INFO_SENSORS, TIME_SENSORS
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
@@ -93,7 +96,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
is_fixable=False,
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
issue_domain=DOMAIN,
|
||||
breaks_in_ha_version="2024.12.0",
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
@@ -115,10 +119,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
"""Set up a configuration entry for Jewish calendar."""
|
||||
language = config_entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE)
|
||||
diaspora = config_entry.data.get(CONF_DIASPORA, DEFAULT_DIASPORA)
|
||||
candle_lighting_offset = config_entry.data.get(
|
||||
candle_lighting_offset = config_entry.options.get(
|
||||
CONF_CANDLE_LIGHT_MINUTES, DEFAULT_CANDLE_LIGHT
|
||||
)
|
||||
havdalah_offset = config_entry.data.get(
|
||||
havdalah_offset = config_entry.options.get(
|
||||
CONF_HAVDALAH_OFFSET_MINUTES, DEFAULT_HAVDALAH_OFFSET_MINUTES
|
||||
)
|
||||
|
||||
@@ -131,19 +135,31 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
timezone=config_entry.data.get(CONF_TIME_ZONE, hass.config.time_zone),
|
||||
)
|
||||
|
||||
prefix = get_unique_prefix(
|
||||
location, language, candle_lighting_offset, havdalah_offset
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = {
|
||||
CONF_LANGUAGE: language,
|
||||
CONF_DIASPORA: diaspora,
|
||||
CONF_LOCATION: location,
|
||||
CONF_CANDLE_LIGHT_MINUTES: candle_lighting_offset,
|
||||
CONF_HAVDALAH_OFFSET_MINUTES: havdalah_offset,
|
||||
"prefix": prefix,
|
||||
}
|
||||
|
||||
# Update unique ID to be unrelated to user defined options
|
||||
old_prefix = get_unique_prefix(
|
||||
location, language, candle_lighting_offset, havdalah_offset
|
||||
)
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
entries = er.async_entries_for_config_entry(ent_reg, config_entry.entry_id)
|
||||
if not entries or any(entry.unique_id.startswith(old_prefix) for entry in entries):
|
||||
async_update_unique_ids(ent_reg, config_entry.entry_id, old_prefix)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
# Trigger update of states for all platforms
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
|
||||
return True
|
||||
|
||||
|
||||
@@ -157,3 +173,25 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
@callback
|
||||
def async_update_unique_ids(
|
||||
ent_reg: er.EntityRegistry, new_prefix: str, old_prefix: str
|
||||
) -> None:
|
||||
"""Update unique ID to be unrelated to user defined options.
|
||||
|
||||
Introduced with release 2024.6
|
||||
"""
|
||||
platform_descriptions = {
|
||||
Platform.BINARY_SENSOR: BINARY_SENSORS,
|
||||
Platform.SENSOR: (*INFO_SENSORS, *TIME_SENSORS),
|
||||
}
|
||||
for platform, descriptions in platform_descriptions.items():
|
||||
for description in descriptions:
|
||||
new_unique_id = f"{new_prefix}-{description.key}"
|
||||
old_unique_id = f"{old_prefix}_{description.key}"
|
||||
if entity_id := ent_reg.async_get_entity_id(
|
||||
platform, DOMAIN, old_unique_id
|
||||
):
|
||||
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
|
||||
@@ -70,10 +70,10 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Jewish Calendar binary sensors."""
|
||||
entry = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
JewishCalendarBinarySensor(
|
||||
hass.data[DOMAIN][config_entry.entry_id], description
|
||||
)
|
||||
JewishCalendarBinarySensor(config_entry.entry_id, entry, description)
|
||||
for description in BINARY_SENSORS
|
||||
)
|
||||
|
||||
@@ -86,13 +86,14 @@ class JewishCalendarBinarySensor(BinarySensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry_id: str,
|
||||
data: dict[str, Any],
|
||||
description: JewishCalendarBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the binary sensor."""
|
||||
self.entity_description = description
|
||||
self._attr_name = f"{DEFAULT_NAME} {description.name}"
|
||||
self._attr_unique_id = f'{data["prefix"]}_{description.key}'
|
||||
self._attr_unique_id = f"{entry_id}-{description.key}"
|
||||
self._location = data[CONF_LOCATION]
|
||||
self._hebrew = data[CONF_LANGUAGE] == "hebrew"
|
||||
self._candle_lighting_offset = data[CONF_CANDLE_LIGHT_MINUTES]
|
||||
|
||||
@@ -100,10 +100,23 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is not None:
|
||||
_options = {}
|
||||
if CONF_CANDLE_LIGHT_MINUTES in user_input:
|
||||
_options[CONF_CANDLE_LIGHT_MINUTES] = user_input[
|
||||
CONF_CANDLE_LIGHT_MINUTES
|
||||
]
|
||||
del user_input[CONF_CANDLE_LIGHT_MINUTES]
|
||||
if CONF_HAVDALAH_OFFSET_MINUTES in user_input:
|
||||
_options[CONF_HAVDALAH_OFFSET_MINUTES] = user_input[
|
||||
CONF_HAVDALAH_OFFSET_MINUTES
|
||||
]
|
||||
del user_input[CONF_HAVDALAH_OFFSET_MINUTES]
|
||||
if CONF_LOCATION in user_input:
|
||||
user_input[CONF_LATITUDE] = user_input[CONF_LOCATION][CONF_LATITUDE]
|
||||
user_input[CONF_LONGITUDE] = user_input[CONF_LOCATION][CONF_LONGITUDE]
|
||||
return self.async_create_entry(title=DEFAULT_NAME, data=user_input)
|
||||
return self.async_create_entry(
|
||||
title=DEFAULT_NAME, data=user_input, options=_options
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
|
||||
@@ -155,9 +155,13 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Jewish calendar sensors ."""
|
||||
entry = hass.data[DOMAIN][config_entry.entry_id]
|
||||
sensors = [JewishCalendarSensor(entry, description) for description in INFO_SENSORS]
|
||||
sensors = [
|
||||
JewishCalendarSensor(config_entry.entry_id, entry, description)
|
||||
for description in INFO_SENSORS
|
||||
]
|
||||
sensors.extend(
|
||||
JewishCalendarTimeSensor(entry, description) for description in TIME_SENSORS
|
||||
JewishCalendarTimeSensor(config_entry.entry_id, entry, description)
|
||||
for description in TIME_SENSORS
|
||||
)
|
||||
|
||||
async_add_entities(sensors)
|
||||
@@ -168,13 +172,14 @@ class JewishCalendarSensor(SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry_id: str,
|
||||
data: dict[str, Any],
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Jewish calendar sensor."""
|
||||
self.entity_description = description
|
||||
self._attr_name = f"{DEFAULT_NAME} {description.name}"
|
||||
self._attr_unique_id = f'{data["prefix"]}_{description.key}'
|
||||
self._attr_unique_id = f"{entry_id}-{description.key}"
|
||||
self._location = data[CONF_LOCATION]
|
||||
self._hebrew = data[CONF_LANGUAGE] == "hebrew"
|
||||
self._candle_lighting_offset = data[CONF_CANDLE_LIGHT_MINUTES]
|
||||
|
||||
@@ -283,16 +283,13 @@ class KNXClimate(KnxEntity, ClimateEntity):
|
||||
)
|
||||
if knx_controller_mode in self._device.mode.controller_modes:
|
||||
await self._device.mode.set_controller_mode(knx_controller_mode)
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if self._device.supports_on_off:
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self._device.turn_off()
|
||||
elif not self._device.is_on:
|
||||
# for default hvac mode, otherwise above would have triggered
|
||||
await self._device.turn_on()
|
||||
self.async_write_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
|
||||
@@ -60,7 +60,9 @@ class KNXNotificationService(BaseNotificationService):
|
||||
|
||||
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a notification to knx bus."""
|
||||
migrate_notify_issue(self.hass, DOMAIN, "KNX", "2024.11.0")
|
||||
migrate_notify_issue(
|
||||
self.hass, DOMAIN, "KNX", "2024.11.0", service_name=self._service_name
|
||||
)
|
||||
if "target" in kwargs:
|
||||
await self._async_send_to_device(message, kwargs["target"])
|
||||
else:
|
||||
|
||||
@@ -23,6 +23,7 @@ turn_on:
|
||||
- light.ColorMode.RGB
|
||||
- light.ColorMode.RGBW
|
||||
- light.ColorMode.RGBWW
|
||||
example: "[255, 100, 100]"
|
||||
selector:
|
||||
color_rgb:
|
||||
rgbw_color:
|
||||
@@ -250,6 +251,7 @@ turn_on:
|
||||
- light.ColorMode.RGB
|
||||
- light.ColorMode.RGBW
|
||||
- light.ColorMode.RGBWW
|
||||
advanced: true
|
||||
selector:
|
||||
color_temp:
|
||||
unit: "mired"
|
||||
@@ -265,7 +267,6 @@ turn_on:
|
||||
- light.ColorMode.RGB
|
||||
- light.ColorMode.RGBW
|
||||
- light.ColorMode.RGBWW
|
||||
advanced: true
|
||||
selector:
|
||||
color_temp:
|
||||
unit: "kelvin"
|
||||
@@ -419,10 +420,35 @@ toggle:
|
||||
- light.ColorMode.RGB
|
||||
- light.ColorMode.RGBW
|
||||
- light.ColorMode.RGBWW
|
||||
advanced: true
|
||||
example: "[255, 100, 100]"
|
||||
selector:
|
||||
color_rgb:
|
||||
rgbw_color:
|
||||
filter:
|
||||
attribute:
|
||||
supported_color_modes:
|
||||
- light.ColorMode.HS
|
||||
- light.ColorMode.XY
|
||||
- light.ColorMode.RGB
|
||||
- light.ColorMode.RGBW
|
||||
- light.ColorMode.RGBWW
|
||||
advanced: true
|
||||
example: "[255, 100, 100, 50]"
|
||||
selector:
|
||||
object:
|
||||
rgbww_color:
|
||||
filter:
|
||||
attribute:
|
||||
supported_color_modes:
|
||||
- light.ColorMode.HS
|
||||
- light.ColorMode.XY
|
||||
- light.ColorMode.RGB
|
||||
- light.ColorMode.RGBW
|
||||
- light.ColorMode.RGBWW
|
||||
advanced: true
|
||||
example: "[255, 100, 100, 50, 70]"
|
||||
selector:
|
||||
object:
|
||||
color_name:
|
||||
filter:
|
||||
attribute:
|
||||
@@ -625,6 +651,9 @@ toggle:
|
||||
advanced: true
|
||||
selector:
|
||||
color_temp:
|
||||
unit: "mired"
|
||||
min: 153
|
||||
max: 500
|
||||
kelvin:
|
||||
filter:
|
||||
attribute:
|
||||
@@ -635,7 +664,6 @@ toggle:
|
||||
- light.ColorMode.RGB
|
||||
- light.ColorMode.RGBW
|
||||
- light.ColorMode.RGBWW
|
||||
advanced: true
|
||||
selector:
|
||||
color_temp:
|
||||
unit: "kelvin"
|
||||
|
||||
@@ -342,6 +342,14 @@
|
||||
"name": "[%key:component::light::services::turn_on::fields::rgb_color::name%]",
|
||||
"description": "[%key:component::light::services::turn_on::fields::rgb_color::description%]"
|
||||
},
|
||||
"rgbw_color": {
|
||||
"name": "[%key:component::light::services::turn_on::fields::rgbw_color::name%]",
|
||||
"description": "[%key:component::light::services::turn_on::fields::rgbw_color::description%]"
|
||||
},
|
||||
"rgbww_color": {
|
||||
"name": "[%key:component::light::services::turn_on::fields::rgbww_color::name%]",
|
||||
"description": "[%key:component::light::services::turn_on::fields::rgbww_color::description%]"
|
||||
},
|
||||
"color_name": {
|
||||
"name": "[%key:component::light::services::turn_on::fields::color_name::name%]",
|
||||
"description": "[%key:component::light::services::turn_on::fields::color_name::description%]"
|
||||
|
||||
@@ -48,6 +48,7 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity):
|
||||
AlarmControlPanelEntityFeature.ARM_HOME
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
)
|
||||
_attr_code_arm_required = False
|
||||
|
||||
def __init__(
|
||||
self, data: lupupy.Lupusec, device: lupupy.devices.LupusecAlarm, entry_id: str
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"dependencies": ["websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["python-matter-server==6.1.0b1"],
|
||||
"requirements": ["python-matter-server==6.1.0"],
|
||||
"zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user