mirror of
https://github.com/home-assistant/core.git
synced 2026-03-30 04:20:22 +02:00
Compare commits
33 Commits
2026.4.0b2
...
rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c830320730 | ||
|
|
336aa0f5df | ||
|
|
754291b34f | ||
|
|
bbae0862b0 | ||
|
|
6b7693b2fd | ||
|
|
954926a05c | ||
|
|
71981f66ec | ||
|
|
7f94f95ac9 | ||
|
|
4ee3177c5d | ||
|
|
9c1f9ca5c6 | ||
|
|
cff4cf4d2c | ||
|
|
ee9d9781ee | ||
|
|
1b972d4adc | ||
|
|
72598479d5 | ||
|
|
02599a4a6e | ||
|
|
af9f351fce | ||
|
|
ff79943776 | ||
|
|
e60048ef30 | ||
|
|
24c0b22038 | ||
|
|
6f32a53742 | ||
|
|
da9d1080d9 | ||
|
|
2ea4d7913e | ||
|
|
16999e3707 | ||
|
|
5c53b847dc | ||
|
|
3afd763d16 | ||
|
|
75a15ed24e | ||
|
|
6d56597a2a | ||
|
|
5872222213 | ||
|
|
bd5c73fd7b | ||
|
|
d8a32dcf69 | ||
|
|
87cd90ab5d | ||
|
|
cb5b0c5b5e | ||
|
|
2fa16101f4 |
@@ -468,6 +468,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> bool:
|
||||
translation.async_setup(hass)
|
||||
|
||||
recovery = hass.config.recovery_mode
|
||||
device_registry.async_setup(hass)
|
||||
try:
|
||||
await asyncio.gather(
|
||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
make_entity_numerical_condition,
|
||||
@@ -59,18 +59,18 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_smoke_cleared": _make_cleared_condition(BinarySensorDeviceClass.SMOKE),
|
||||
# Numerical sensor conditions with unit conversion
|
||||
"is_co_value": make_entity_numerical_condition_with_unit(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"is_ozone_value": make_entity_numerical_condition_with_unit(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
OzoneConcentrationConverter,
|
||||
),
|
||||
"is_voc_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||
)
|
||||
},
|
||||
@@ -79,7 +79,7 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
),
|
||||
"is_voc_ratio_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
},
|
||||
@@ -87,59 +87,43 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
UnitlessRatioConverter,
|
||||
),
|
||||
"is_no_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
),
|
||||
"is_no2_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
),
|
||||
"is_so2_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
),
|
||||
# Numerical sensor conditions without unit conversion (single-unit device classes)
|
||||
"is_co2_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"is_pm1_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm25_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm4_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm10_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_n2o_value": make_entity_numerical_condition(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROUS_OXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
EntityTargetStateTriggerBase,
|
||||
Trigger,
|
||||
@@ -64,28 +64,28 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"smoke_cleared": _make_cleared_trigger(BinarySensorDeviceClass.SMOKE),
|
||||
# Numerical sensor triggers with unit conversion
|
||||
"co_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"co_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"ozone_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
OzoneConcentrationConverter,
|
||||
),
|
||||
"ozone_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
OzoneConcentrationConverter,
|
||||
),
|
||||
"voc_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||
)
|
||||
},
|
||||
@@ -94,7 +94,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
),
|
||||
"voc_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||
)
|
||||
},
|
||||
@@ -103,7 +103,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
),
|
||||
"voc_ratio_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
},
|
||||
@@ -112,7 +112,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
),
|
||||
"voc_ratio_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
},
|
||||
@@ -120,114 +120,82 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
UnitlessRatioConverter,
|
||||
),
|
||||
"no_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
),
|
||||
"no_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
),
|
||||
"no2_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
),
|
||||
"no2_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
),
|
||||
"so2_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
),
|
||||
"so2_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
),
|
||||
# Numerical sensor triggers without unit conversion (single-unit device classes)
|
||||
"co2_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"co2_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"pm1_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm1_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm25_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm25_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm4_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm4_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm10_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm10_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"n2o_changed": make_entity_numerical_state_changed_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROUS_OXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"n2o_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROUS_OXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==13.3.0"]
|
||||
"requirements": ["aioamazondevices==13.3.1"]
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"person",
|
||||
"power",
|
||||
"schedule",
|
||||
"select",
|
||||
"siren",
|
||||
"switch",
|
||||
"temperature",
|
||||
@@ -186,6 +187,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"switch",
|
||||
"temperature",
|
||||
"text",
|
||||
"todo",
|
||||
"update",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
|
||||
@@ -6,7 +6,6 @@ from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -27,7 +26,6 @@ BATTERY_CHARGING_DOMAIN_SPECS = {
|
||||
}
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS = {
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
|
||||
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.BATTERY),
|
||||
}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
|
||||
@@ -53,8 +53,6 @@ is_level:
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: battery
|
||||
- domain: number
|
||||
device_class: battery
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
|
||||
@@ -6,11 +6,10 @@ from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
make_entity_numerical_state_changed_trigger,
|
||||
@@ -28,9 +27,8 @@ BATTERY_CHARGING_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
),
|
||||
}
|
||||
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS: dict[str, NumericalDomainSpec] = {
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.BATTERY),
|
||||
NUMBER_DOMAIN: NumericalDomainSpec(device_class=NumberDeviceClass.BATTERY),
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
|
||||
}
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
|
||||
@@ -38,8 +38,6 @@
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: battery
|
||||
- domain: number
|
||||
device_class: battery
|
||||
|
||||
low:
|
||||
fields:
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
"""Provides conditions for climates."""
|
||||
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
|
||||
Condition,
|
||||
ConditionConfig,
|
||||
EntityConditionBase,
|
||||
EntityNumericalConditionWithUnitBase,
|
||||
make_entity_numerical_condition,
|
||||
make_entity_state_condition,
|
||||
@@ -13,12 +21,42 @@ from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
||||
|
||||
CONF_HVAC_MODE = "hvac_mode"
|
||||
|
||||
_HVAC_MODE_CONDITION_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_HVAC_MODE): vol.All(
|
||||
cv.ensure_list, vol.Length(min=1), [vol.Coerce(HVACMode)]
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ClimateHVACModeCondition(EntityConditionBase):
|
||||
"""Condition for climate HVAC mode."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
_schema = _HVAC_MODE_CONDITION_SCHEMA
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
||||
"""Initialize the HVAC mode condition."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
self._hvac_modes: set[str] = set(config.options[CONF_HVAC_MODE])
|
||||
|
||||
def is_valid_state(self, entity_state: State) -> bool:
|
||||
"""Check if the state matches any of the expected HVAC modes."""
|
||||
return entity_state.state in self._hvac_modes
|
||||
|
||||
|
||||
class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
|
||||
"""Mixin for climate target temperature conditions with unit conversion."""
|
||||
|
||||
_base_unit = UnitOfTemperature.CELSIUS
|
||||
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_unit_converter = TemperatureConverter
|
||||
|
||||
def _get_entity_unit(self, entity_state: State) -> str | None:
|
||||
@@ -28,6 +66,7 @@ class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_hvac_mode": ClimateHVACModeCondition,
|
||||
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
|
||||
"is_on": make_entity_state_condition(
|
||||
DOMAIN,
|
||||
@@ -50,7 +89,7 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
|
||||
),
|
||||
"target_humidity": make_entity_numerical_condition(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"target_temperature": ClimateTargetTemperatureCondition,
|
||||
|
||||
@@ -45,6 +45,21 @@ is_cooling: *condition_common
|
||||
is_drying: *condition_common
|
||||
is_heating: *condition_common
|
||||
|
||||
is_hvac_mode:
|
||||
target: *condition_climate_target
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
hvac_mode:
|
||||
context:
|
||||
filter_target: target
|
||||
required: true
|
||||
selector:
|
||||
state:
|
||||
hide_states:
|
||||
- unavailable
|
||||
- unknown
|
||||
multiple: true
|
||||
|
||||
target_humidity:
|
||||
target: *condition_climate_target
|
||||
fields:
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
"is_heating": {
|
||||
"condition": "mdi:fire"
|
||||
},
|
||||
"is_hvac_mode": {
|
||||
"condition": "mdi:thermostat"
|
||||
},
|
||||
"is_off": {
|
||||
"condition": "mdi:power-off"
|
||||
},
|
||||
|
||||
@@ -41,6 +41,20 @@
|
||||
},
|
||||
"name": "Climate-control device is heating"
|
||||
},
|
||||
"is_hvac_mode": {
|
||||
"description": "Tests if one or more climate-control devices are set to a specific HVAC mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
},
|
||||
"hvac_mode": {
|
||||
"description": "The HVAC modes to test for.",
|
||||
"name": "Modes"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device HVAC mode"
|
||||
},
|
||||
"is_off": {
|
||||
"description": "Tests if one or more climate-control devices are off.",
|
||||
"fields": {
|
||||
|
||||
@@ -5,7 +5,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||
@@ -52,7 +52,7 @@ class _ClimateTargetTemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitB
|
||||
"""Mixin for climate target temperature triggers with unit conversion."""
|
||||
|
||||
_base_unit = UnitOfTemperature.CELSIUS
|
||||
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_unit_converter = TemperatureConverter
|
||||
|
||||
def _get_entity_unit(self, state: State) -> str | None:
|
||||
@@ -84,11 +84,11 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.DRYING
|
||||
),
|
||||
"target_humidity_changed": make_entity_numerical_state_changed_trigger(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"target_temperature_changed": ClimateTargetTemperatureChangedTrigger,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Provides conditions for covers."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.condition import Condition, EntityConditionBase
|
||||
@@ -8,9 +10,11 @@ from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
|
||||
from .models import CoverDomainSpec
|
||||
|
||||
|
||||
class CoverConditionBase(EntityConditionBase[CoverDomainSpec]):
|
||||
class CoverConditionBase(EntityConditionBase):
|
||||
"""Base condition for cover state checks."""
|
||||
|
||||
_domain_specs: Mapping[str, CoverDomainSpec]
|
||||
|
||||
def is_valid_state(self, entity_state: State) -> bool:
|
||||
"""Check if the state matches the expected cover state."""
|
||||
domain_spec = self._domain_specs[entity_state.domain]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Provides triggers for covers."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.trigger import EntityTriggerBase, Trigger
|
||||
@@ -8,9 +10,11 @@ from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
|
||||
from .models import CoverDomainSpec
|
||||
|
||||
|
||||
class CoverTriggerBase(EntityTriggerBase[CoverDomainSpec]):
|
||||
class CoverTriggerBase(EntityTriggerBase):
|
||||
"""Base trigger for cover state changes."""
|
||||
|
||||
_domain_specs: Mapping[str, CoverDomainSpec]
|
||||
|
||||
def _get_value(self, state: State) -> str | bool | None:
|
||||
"""Extract the relevant value from state based on domain spec."""
|
||||
domain_spec = self._domain_specs[state.domain]
|
||||
|
||||
@@ -51,7 +51,6 @@ def _entity_entry_filter(a: attr.Attribute, _: Any) -> bool:
|
||||
return a.name not in (
|
||||
"_cache",
|
||||
"compat_aliases",
|
||||
"compat_name",
|
||||
"original_name_unprefixed",
|
||||
)
|
||||
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260325.0"]
|
||||
"requirements": ["home-assistant-frontend==20260325.2"]
|
||||
}
|
||||
|
||||
@@ -1,15 +1,73 @@
|
||||
"""Provides conditions for humidifiers."""
|
||||
|
||||
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_MODE, CONF_OPTIONS, PERCENTAGE, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
|
||||
Condition,
|
||||
ConditionConfig,
|
||||
EntityStateConditionBase,
|
||||
make_entity_numerical_condition,
|
||||
make_entity_state_condition,
|
||||
)
|
||||
from homeassistant.helpers.entity import get_supported_features
|
||||
|
||||
from .const import (
|
||||
ATTR_ACTION,
|
||||
ATTR_HUMIDITY,
|
||||
DOMAIN,
|
||||
HumidifierAction,
|
||||
HumidifierEntityFeature,
|
||||
)
|
||||
|
||||
CONF_MODE = "mode"
|
||||
|
||||
IS_MODE_CONDITION_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_MODE): vol.All(cv.ensure_list, vol.Length(min=1), [str]),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
|
||||
"""Test if an entity supports the specified features."""
|
||||
try:
|
||||
return bool(get_supported_features(hass, entity_id) & features)
|
||||
except HomeAssistantError:
|
||||
return False
|
||||
|
||||
|
||||
class IsModeCondition(EntityStateConditionBase):
|
||||
"""Condition for humidifier mode."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_MODE)}
|
||||
_schema = IS_MODE_CONDITION_SCHEMA
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
||||
"""Initialize the mode condition."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
self._states = set(config.options[CONF_MODE])
|
||||
|
||||
def entity_filter(self, entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
entities = super().entity_filter(entities)
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if _supports_feature(self._hass, entity_id, HumidifierEntityFeature.MODES)
|
||||
}
|
||||
|
||||
from .const import ATTR_ACTION, ATTR_HUMIDITY, DOMAIN, HumidifierAction
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
@@ -20,8 +78,9 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_humidifying": make_entity_state_condition(
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_ACTION)}, HumidifierAction.HUMIDIFYING
|
||||
),
|
||||
"is_mode": IsModeCondition,
|
||||
"is_target_humidity": make_entity_numerical_condition(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit=PERCENTAGE,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -32,6 +32,19 @@ is_on: *condition_common
|
||||
is_drying: *condition_common
|
||||
is_humidifying: *condition_common
|
||||
|
||||
is_mode:
|
||||
target: *condition_humidifier_target
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
mode:
|
||||
context:
|
||||
filter_target: target
|
||||
required: true
|
||||
selector:
|
||||
state:
|
||||
attribute: available_modes
|
||||
multiple: true
|
||||
|
||||
is_target_humidity:
|
||||
target: *condition_humidifier_target
|
||||
fields:
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"is_humidifying": {
|
||||
"condition": "mdi:arrow-up-bold"
|
||||
},
|
||||
"is_mode": {
|
||||
"condition": "mdi:air-humidifier"
|
||||
},
|
||||
"is_off": {
|
||||
"condition": "mdi:air-humidifier-off"
|
||||
},
|
||||
|
||||
@@ -28,6 +28,20 @@
|
||||
},
|
||||
"name": "Humidifier is humidifying"
|
||||
},
|
||||
"is_mode": {
|
||||
"description": "Tests if one or more humidifiers are set to a specific mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::humidifier::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::humidifier::common::condition_behavior_name%]"
|
||||
},
|
||||
"mode": {
|
||||
"description": "The operation modes to check for.",
|
||||
"name": "Mode"
|
||||
}
|
||||
},
|
||||
"name": "Humidifier is in mode"
|
||||
},
|
||||
"is_off": {
|
||||
"description": "Tests if one or more humidifiers are off.",
|
||||
"fields": {
|
||||
|
||||
@@ -10,22 +10,27 @@ from homeassistant.components.humidifier import (
|
||||
ATTR_CURRENT_HUMIDITY as HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
|
||||
DOMAIN as HUMIDIFIER_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_WEATHER_HUMIDITY,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import Condition, make_entity_numerical_condition
|
||||
|
||||
HUMIDITY_DOMAIN_SPECS = {
|
||||
CLIMATE_DOMAIN: NumericalDomainSpec(
|
||||
CLIMATE_DOMAIN: DomainSpec(
|
||||
value_source=CLIMATE_ATTR_CURRENT_HUMIDITY,
|
||||
),
|
||||
HUMIDIFIER_DOMAIN: NumericalDomainSpec(
|
||||
HUMIDIFIER_DOMAIN: DomainSpec(
|
||||
value_source=HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
|
||||
),
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.HUMIDITY),
|
||||
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.HUMIDITY),
|
||||
WEATHER_DOMAIN: DomainSpec(
|
||||
value_source=ATTR_WEATHER_HUMIDITY,
|
||||
),
|
||||
}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
|
||||
@@ -17,10 +17,9 @@ is_value:
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: humidity
|
||||
- domain: number
|
||||
device_class: humidity
|
||||
- domain: climate
|
||||
- domain: humidifier
|
||||
- domain: weather
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
|
||||
@@ -16,24 +16,24 @@ from homeassistant.components.weather import (
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
make_entity_numerical_state_changed_trigger,
|
||||
make_entity_numerical_state_crossed_threshold_trigger,
|
||||
)
|
||||
|
||||
HUMIDITY_DOMAIN_SPECS: dict[str, NumericalDomainSpec] = {
|
||||
CLIMATE_DOMAIN: NumericalDomainSpec(
|
||||
HUMIDITY_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
CLIMATE_DOMAIN: DomainSpec(
|
||||
value_source=CLIMATE_ATTR_CURRENT_HUMIDITY,
|
||||
),
|
||||
HUMIDIFIER_DOMAIN: NumericalDomainSpec(
|
||||
HUMIDIFIER_DOMAIN: DomainSpec(
|
||||
value_source=HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
|
||||
),
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
),
|
||||
WEATHER_DOMAIN: NumericalDomainSpec(
|
||||
WEATHER_DOMAIN: DomainSpec(
|
||||
value_source=ATTR_WEATHER_HUMIDITY,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["idasen-ha==2.6.4"]
|
||||
"requirements": ["idasen-ha==2.6.5"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import LIGHT_LUX, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -22,7 +21,6 @@ ILLUMINANCE_DETECTED_DOMAIN_SPECS = {
|
||||
}
|
||||
ILLUMINANCE_VALUE_DOMAIN_SPECS = {
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.ILLUMINANCE),
|
||||
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.ILLUMINANCE),
|
||||
}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
|
||||
@@ -23,8 +23,6 @@ is_value:
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: illuminance
|
||||
- domain: number
|
||||
device_class: illuminance
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
|
||||
@@ -6,11 +6,10 @@ from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import LIGHT_LUX, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
make_entity_numerical_state_changed_trigger,
|
||||
@@ -18,9 +17,8 @@ from homeassistant.helpers.trigger import (
|
||||
make_entity_target_state_trigger,
|
||||
)
|
||||
|
||||
ILLUMINANCE_DOMAIN_SPECS: dict[str, NumericalDomainSpec] = {
|
||||
NUMBER_DOMAIN: NumericalDomainSpec(device_class=NumberDeviceClass.ILLUMINANCE),
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.ILLUMINANCE),
|
||||
ILLUMINANCE_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.ILLUMINANCE),
|
||||
}
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
|
||||
.trigger_numerical_target: &trigger_numerical_target
|
||||
entity:
|
||||
- domain: number
|
||||
device_class: illuminance
|
||||
- domain: sensor
|
||||
device_class: illuminance
|
||||
|
||||
|
||||
@@ -1,12 +1,43 @@
|
||||
"""Provides conditions for lights."""
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
EntityNumericalConditionBase,
|
||||
make_entity_state_condition,
|
||||
)
|
||||
|
||||
from . import ATTR_BRIGHTNESS
|
||||
from .const import DOMAIN
|
||||
|
||||
BRIGHTNESS_DOMAIN_SPECS = {
|
||||
DOMAIN: DomainSpec(value_source=ATTR_BRIGHTNESS),
|
||||
}
|
||||
|
||||
|
||||
class BrightnessCondition(EntityNumericalConditionBase):
|
||||
"""Condition for light brightness with uint8 to percentage conversion."""
|
||||
|
||||
_domain_specs = BRIGHTNESS_DOMAIN_SPECS
|
||||
_valid_unit = "%"
|
||||
|
||||
def _get_tracked_value(self, entity_state: State) -> Any:
|
||||
"""Get the brightness value converted from uint8 (0-255) to percentage (0-100)."""
|
||||
raw = super()._get_tracked_value(entity_state)
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
return (float(raw) / 255.0) * 100.0
|
||||
except TypeError, ValueError:
|
||||
return None
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_brightness": BrightnessCondition,
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.condition_common: &condition_common
|
||||
target:
|
||||
target: &condition_light_target
|
||||
entity:
|
||||
domain: light
|
||||
fields:
|
||||
behavior:
|
||||
behavior: &condition_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
@@ -13,5 +13,31 @@
|
||||
- all
|
||||
- any
|
||||
|
||||
.brightness_threshold_entity: &brightness_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
unit_of_measurement: "%"
|
||||
|
||||
.brightness_threshold_number: &brightness_threshold_number
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
is_brightness:
|
||||
target: *condition_light_target
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *brightness_threshold_entity
|
||||
mode: is
|
||||
number: *brightness_threshold_number
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_brightness": {
|
||||
"condition": "mdi:lightbulb-on-50"
|
||||
},
|
||||
"is_off": {
|
||||
"condition": "mdi:lightbulb-off"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted lights.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"condition_threshold_description": "What to test for and threshold values.",
|
||||
"condition_threshold_name": "Threshold configuration",
|
||||
"field_brightness_description": "Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness, and 255 is the maximum brightness.",
|
||||
"field_brightness_name": "Brightness value",
|
||||
"field_brightness_pct_description": "Number indicating the percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness, and 100 is the maximum brightness.",
|
||||
@@ -42,6 +44,20 @@
|
||||
"trigger_threshold_name": "Threshold configuration"
|
||||
},
|
||||
"conditions": {
|
||||
"is_brightness": {
|
||||
"description": "Tests the brightness of one or more lights.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::light::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::light::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"description": "[%key:component::light::common::condition_threshold_description%]",
|
||||
"name": "[%key:component::light::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Light brightness"
|
||||
},
|
||||
"is_off": {
|
||||
"description": "Tests if one or more lights are off.",
|
||||
"fields": {
|
||||
|
||||
@@ -1,40 +1,54 @@
|
||||
"""Provides triggers for lights."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import NumericalDomainSpec
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
EntityNumericalStateChangedTriggerBase,
|
||||
EntityNumericalStateCrossedThresholdTriggerBase,
|
||||
EntityNumericalStateTriggerBase,
|
||||
Trigger,
|
||||
make_entity_numerical_state_changed_trigger,
|
||||
make_entity_numerical_state_crossed_threshold_trigger,
|
||||
make_entity_target_state_trigger,
|
||||
)
|
||||
|
||||
from . import ATTR_BRIGHTNESS
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def _convert_uint8_to_percentage(value: Any) -> float:
|
||||
"""Convert a uint8 value (0-255) to a percentage (0-100)."""
|
||||
return (float(value) / 255.0) * 100.0
|
||||
|
||||
|
||||
BRIGHTNESS_DOMAIN_SPECS = {
|
||||
DOMAIN: NumericalDomainSpec(
|
||||
value_source=ATTR_BRIGHTNESS,
|
||||
value_converter=_convert_uint8_to_percentage,
|
||||
),
|
||||
DOMAIN: DomainSpec(value_source=ATTR_BRIGHTNESS),
|
||||
}
|
||||
|
||||
|
||||
class BrightnessTriggerMixin(EntityNumericalStateTriggerBase):
|
||||
"""Mixin for brightness triggers."""
|
||||
|
||||
_domain_specs = BRIGHTNESS_DOMAIN_SPECS
|
||||
_valid_unit = "%"
|
||||
|
||||
def _get_tracked_value(self, state: State) -> float | None:
|
||||
"""Get tracked brightness as a percentage."""
|
||||
value = super()._get_tracked_value(state)
|
||||
if value is None:
|
||||
return None
|
||||
# Convert uint8 value (0-255) to a percentage (0-100)
|
||||
return (value / 255.0) * 100.0
|
||||
|
||||
|
||||
class BrightnessChangedTrigger(
|
||||
EntityNumericalStateChangedTriggerBase, BrightnessTriggerMixin
|
||||
):
|
||||
"""Trigger for light brightness changes."""
|
||||
|
||||
|
||||
class BrightnessCrossedThresholdTrigger(
|
||||
EntityNumericalStateCrossedThresholdTriggerBase, BrightnessTriggerMixin
|
||||
):
|
||||
"""Trigger for light brightness crossing a threshold."""
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"brightness_changed": make_entity_numerical_state_changed_trigger(
|
||||
BRIGHTNESS_DOMAIN_SPECS, valid_unit="%"
|
||||
),
|
||||
"brightness_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
BRIGHTNESS_DOMAIN_SPECS, valid_unit="%"
|
||||
),
|
||||
"brightness_changed": BrightnessChangedTrigger,
|
||||
"brightness_crossed_threshold": BrightnessCrossedThresholdTrigger,
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["lojack_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["lojack-api==0.7.1"]
|
||||
"requirements": ["lojack-api==0.7.2"]
|
||||
}
|
||||
|
||||
@@ -270,6 +270,7 @@ class ProgramPhaseOven(MieleEnum, missing_to_none=True):
|
||||
process_finished = 3078
|
||||
searing = 3080
|
||||
roasting = 3081
|
||||
cooling_down = 3083
|
||||
energy_save = 3084
|
||||
pre_heating = 3099
|
||||
|
||||
@@ -452,6 +453,7 @@ class WashingMachineProgramId(MieleEnum, missing_to_none=True):
|
||||
proofing = 27, 10057
|
||||
sportswear = 29, 10052
|
||||
automatic_plus = 31
|
||||
table_linen = 33
|
||||
outerwear = 37
|
||||
pillows = 39
|
||||
cool_air = 45 # washer-dryer
|
||||
@@ -586,6 +588,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
microwave_fan_grill = 23
|
||||
conventional_heat = 24
|
||||
top_heat = 25
|
||||
booster = 27
|
||||
fan_grill = 29
|
||||
bottom_heat = 31
|
||||
moisture_plus_auto_roast = 35, 48
|
||||
@@ -594,6 +597,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
moisture_plus_conventional_heat = 51, 76
|
||||
popcorn = 53
|
||||
quick_microwave = 54
|
||||
airfry = 95
|
||||
custom_program_1 = 97
|
||||
custom_program_2 = 98
|
||||
custom_program_3 = 99
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
"program_id": {
|
||||
"name": "Program",
|
||||
"state": {
|
||||
"airfry": "AirFry",
|
||||
"almond_macaroons_1_tray": "Almond macaroons (1 tray)",
|
||||
"almond_macaroons_2_trays": "Almond macaroons (2 trays)",
|
||||
"amaranth": "Amaranth",
|
||||
@@ -334,6 +335,7 @@
|
||||
"blanching": "Blanching",
|
||||
"blueberry_muffins": "Blueberry muffins",
|
||||
"bologna_sausage": "Bologna sausage",
|
||||
"booster": "Booster",
|
||||
"bottling": "Bottling",
|
||||
"bottling_hard": "Bottling (hard)",
|
||||
"bottling_medium": "Bottling (medium)",
|
||||
@@ -881,6 +883,7 @@
|
||||
"swiss_roll": "Swiss roll",
|
||||
"swiss_toffee_cream_100_ml": "Swiss toffee cream (100 ml)",
|
||||
"swiss_toffee_cream_150_ml": "Swiss toffee cream (150 ml)",
|
||||
"table_linen": "Table linen",
|
||||
"tagliatelli_fresh": "Tagliatelli (fresh)",
|
||||
"tall_items": "Tall items",
|
||||
"tart_flambe": "Tart flambè",
|
||||
|
||||
@@ -6,7 +6,6 @@ from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -25,7 +24,6 @@ _MOISTURE_BINARY_DOMAIN_SPECS = {
|
||||
|
||||
_MOISTURE_NUMERICAL_DOMAIN_SPECS = {
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.MOISTURE),
|
||||
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.MOISTURE),
|
||||
}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
|
||||
@@ -37,8 +37,6 @@ is_value:
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: moisture
|
||||
- domain: number
|
||||
device_class: moisture
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
|
||||
@@ -6,11 +6,10 @@ from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
make_entity_numerical_state_changed_trigger,
|
||||
@@ -22,9 +21,8 @@ MOISTURE_BINARY_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.MOISTURE),
|
||||
}
|
||||
|
||||
MOISTURE_NUMERICAL_DOMAIN_SPECS: dict[str, NumericalDomainSpec] = {
|
||||
NUMBER_DOMAIN: NumericalDomainSpec(device_class=NumberDeviceClass.MOISTURE),
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.MOISTURE),
|
||||
MOISTURE_NUMERICAL_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.MOISTURE),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
|
||||
.trigger_numerical_target: &trigger_numerical_target
|
||||
entity:
|
||||
- domain: number
|
||||
device_class: moisture
|
||||
- domain: sensor
|
||||
device_class: moisture
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-pooldose==0.8.6"]
|
||||
"requirements": ["python-pooldose==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
make_entity_numerical_condition_with_unit,
|
||||
@@ -14,8 +13,7 @@ from homeassistant.helpers.condition import (
|
||||
from homeassistant.util.unit_conversion import PowerConverter
|
||||
|
||||
POWER_DOMAIN_SPECS = {
|
||||
NUMBER_DOMAIN: NumericalDomainSpec(device_class=NumberDeviceClass.POWER),
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.POWER),
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.POWER),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
is_value:
|
||||
target:
|
||||
entity:
|
||||
- domain: number
|
||||
device_class: power
|
||||
- domain: sensor
|
||||
device_class: power
|
||||
fields:
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
make_entity_numerical_state_changed_with_unit_trigger,
|
||||
@@ -14,9 +13,8 @@ from homeassistant.helpers.trigger import (
|
||||
)
|
||||
from homeassistant.util.unit_conversion import PowerConverter
|
||||
|
||||
POWER_DOMAIN_SPECS: dict[str, NumericalDomainSpec] = {
|
||||
NUMBER_DOMAIN: NumericalDomainSpec(device_class=NumberDeviceClass.POWER),
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.POWER),
|
||||
POWER_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.POWER),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
|
||||
.trigger_target: &trigger_target
|
||||
entity:
|
||||
- domain: number
|
||||
device_class: power
|
||||
- domain: sensor
|
||||
device_class: power
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, ProxmoxPermission
|
||||
from .coordinator import ProxmoxConfigEntry, ProxmoxCoordinator, ProxmoxNodeData
|
||||
from .entity import ProxmoxContainerEntity, ProxmoxNodeEntity, ProxmoxVMEntity
|
||||
from .helpers import is_granted
|
||||
@@ -34,6 +34,8 @@ class ProxmoxNodeButtonNodeEntityDescription(ButtonEntityDescription):
|
||||
"""Class to hold Proxmox node button description."""
|
||||
|
||||
press_action: Callable[[ProxmoxCoordinator, str], None]
|
||||
permission: ProxmoxPermission = ProxmoxPermission.POWER
|
||||
permission_raise: str = "no_permission_node_power"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -41,6 +43,8 @@ class ProxmoxVMButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Class to hold Proxmox VM button description."""
|
||||
|
||||
press_action: Callable[[ProxmoxCoordinator, str, int], None]
|
||||
permission: ProxmoxPermission = ProxmoxPermission.POWER
|
||||
permission_raise: str = "no_permission_vm_lxc_power"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -48,6 +52,8 @@ class ProxmoxContainerButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Class to hold Proxmox container button description."""
|
||||
|
||||
press_action: Callable[[ProxmoxCoordinator, str, int], None]
|
||||
permission: ProxmoxPermission = ProxmoxPermission.POWER
|
||||
permission_raise: str = "no_permission_vm_lxc_power"
|
||||
|
||||
|
||||
NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
|
||||
@@ -156,6 +162,8 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
|
||||
)
|
||||
)
|
||||
),
|
||||
permission=ProxmoxPermission.SNAPSHOT,
|
||||
permission_raise="no_permission_snapshot",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
@@ -199,6 +207,8 @@ CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
|
||||
)
|
||||
)
|
||||
),
|
||||
permission=ProxmoxPermission.SNAPSHOT,
|
||||
permission_raise="no_permission_snapshot",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
@@ -315,10 +325,15 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the node button action via executor."""
|
||||
node_id = self._node_data.node["node"]
|
||||
if not is_granted(self.coordinator.permissions, p_type="nodes", p_id=node_id):
|
||||
if not is_granted(
|
||||
self.coordinator.permissions,
|
||||
p_type="nodes",
|
||||
p_id=node_id,
|
||||
permission=self.entity_description.permission,
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_node_power",
|
||||
translation_key=self.entity_description.permission_raise,
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.entity_description.press_action,
|
||||
@@ -335,10 +350,15 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the VM button action via executor."""
|
||||
vmid = self.vm_data["vmid"]
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
|
||||
if not is_granted(
|
||||
self.coordinator.permissions,
|
||||
p_type="vms",
|
||||
p_id=vmid,
|
||||
permission=self.entity_description.permission,
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_vm_lxc_power",
|
||||
translation_key=self.entity_description.permission_raise,
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.entity_description.press_action,
|
||||
@@ -357,10 +377,15 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
|
||||
"""Execute the container button action via executor."""
|
||||
vmid = self.container_data["vmid"]
|
||||
# Container power actions fall under vms
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
|
||||
if not is_granted(
|
||||
self.coordinator.permissions,
|
||||
p_type="vms",
|
||||
p_id=vmid,
|
||||
permission=self.entity_description.permission,
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_vm_lxc_power",
|
||||
translation_key=self.entity_description.permission_raise,
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.entity_description.press_action,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Constants for ProxmoxVE."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
DOMAIN = "proxmoxve"
|
||||
CONF_AUTH_METHOD = "auth_method"
|
||||
CONF_REALM = "realm"
|
||||
@@ -33,4 +35,9 @@ TYPE_VM = 0
|
||||
TYPE_CONTAINER = 1
|
||||
UPDATE_INTERVAL = 60
|
||||
|
||||
PERM_POWER = "VM.PowerMgmt"
|
||||
|
||||
class ProxmoxPermission(StrEnum):
|
||||
"""Proxmox permissions."""
|
||||
|
||||
POWER = "VM.PowerMgmt"
|
||||
SNAPSHOT = "VM.Snapshot"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Helpers for Proxmox VE."""
|
||||
|
||||
from .const import PERM_POWER
|
||||
from .const import ProxmoxPermission
|
||||
|
||||
|
||||
def is_granted(
|
||||
permissions: dict[str, dict[str, int]],
|
||||
p_type: str = "vms",
|
||||
p_id: str | int | None = None, # can be str for nodes
|
||||
permission: str = PERM_POWER,
|
||||
permission: ProxmoxPermission = ProxmoxPermission.POWER,
|
||||
) -> bool:
|
||||
"""Validate user permissions for the given type and permission."""
|
||||
paths = [f"/{p_type}/{p_id}", f"/{p_type}", "/"]
|
||||
|
||||
@@ -315,6 +315,9 @@
|
||||
"no_permission_node_power": {
|
||||
"message": "The configured Proxmox VE user does not have permission to manage the power state of nodes. Please grant the user the 'VM.PowerMgmt' permission and try again."
|
||||
},
|
||||
"no_permission_snapshot": {
|
||||
"message": "The configured Proxmox VE user does not have permission to create snapshots of VMs and containers. Please grant the user the 'VM.Snapshot' permission and try again."
|
||||
},
|
||||
"no_permission_vm_lxc_power": {
|
||||
"message": "The configured Proxmox VE user does not have permission to manage the power state of VMs and containers. Please grant the user the 'VM.PowerMgmt' permission and try again."
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@ CV_WX_DATA_VALID_TEMP_RANGE = vol.All(vol.Coerce(float), vol.Range(min=-40.0, ma
|
||||
CV_WX_DATA_VALID_RAIN_RANGE = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1000.0))
|
||||
CV_WX_DATA_VALID_WIND_SPEED = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=65.0))
|
||||
CV_WX_DATA_VALID_PRESSURE = vol.All(vol.Coerce(float), vol.Range(min=60.0, max=110.0))
|
||||
CV_WX_DATA_VALID_SOLARRAD = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=5.0))
|
||||
CV_WX_DATA_VALID_SOLARRAD = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=100.0))
|
||||
|
||||
SERVICE_NAME_PAUSE_WATERING = "pause_watering"
|
||||
SERVICE_NAME_PUSH_FLOW_METER_DATA = "push_flow_meter_data"
|
||||
|
||||
@@ -131,9 +131,9 @@ push_weather_data:
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 5
|
||||
max: 100
|
||||
step: 0.1
|
||||
unit_of_measurement: "MJ/m²/h"
|
||||
unit_of_measurement: "MJ/m²/d"
|
||||
et:
|
||||
selector:
|
||||
number:
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
"name": "Measured rainfall"
|
||||
},
|
||||
"solarrad": {
|
||||
"description": "Current solar radiation (MJ/m²/h).",
|
||||
"description": "Daily solar radiation (MJ/m²/d).",
|
||||
"name": "Solar radiation"
|
||||
},
|
||||
"temperature": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["renault_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["renault-api==0.5.6"]
|
||||
"requirements": ["renault-api==0.5.7"]
|
||||
}
|
||||
|
||||
55
homeassistant/components/select/condition.py
Normal file
55
homeassistant/components/select/condition.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Provides conditions for selects."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.input_select import DOMAIN as INPUT_SELECT_DOMAIN
|
||||
from homeassistant.const import CONF_OPTIONS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
|
||||
Condition,
|
||||
ConditionConfig,
|
||||
EntityStateConditionBase,
|
||||
)
|
||||
|
||||
from .const import CONF_OPTION, DOMAIN
|
||||
|
||||
IS_OPTION_SELECTED_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_OPTION): vol.All(
|
||||
cv.ensure_list, vol.Length(min=1), [str]
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
SELECT_DOMAIN_SPECS = {DOMAIN: DomainSpec(), INPUT_SELECT_DOMAIN: DomainSpec()}
|
||||
|
||||
|
||||
class IsOptionSelectedCondition(EntityStateConditionBase):
|
||||
"""Condition for select option."""
|
||||
|
||||
_domain_specs = SELECT_DOMAIN_SPECS
|
||||
_schema = IS_OPTION_SELECTED_SCHEMA
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
||||
"""Initialize the option selected condition."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
self._states = set(config.options[CONF_OPTION])
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_option_selected": IsOptionSelectedCondition,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the select conditions."""
|
||||
return CONDITIONS
|
||||
26
homeassistant/components/select/conditions.yaml
Normal file
26
homeassistant/components/select/conditions.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
is_option_selected:
|
||||
target:
|
||||
entity:
|
||||
- domain: select
|
||||
- domain: input_select
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
option:
|
||||
context:
|
||||
filter_target: target
|
||||
required: true
|
||||
selector:
|
||||
state:
|
||||
attribute: options
|
||||
hide_states:
|
||||
- unavailable
|
||||
- unknown
|
||||
multiple: true
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_option_selected": {
|
||||
"condition": "mdi:format-list-bulleted"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:format-list-bulleted"
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_option_selected": {
|
||||
"description": "Tests if one or more dropdowns have a specific option selected.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "Whether the condition should pass when any or all targeted entities match.",
|
||||
"name": "Behavior"
|
||||
},
|
||||
"option": {
|
||||
"description": "The options to check for.",
|
||||
"name": "Option"
|
||||
}
|
||||
},
|
||||
"name": "Option is selected"
|
||||
}
|
||||
},
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"select_first": "Change {entity_name} to first option",
|
||||
@@ -36,6 +52,14 @@
|
||||
"message": "Option {option} is not valid for entity {entity_id}, valid options are: {options}."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"select_first": {
|
||||
"description": "Selects the first option of a select.",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"name": "[%key:component::siren::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "If a siren is off"
|
||||
"name": "Siren is off"
|
||||
},
|
||||
"is_on": {
|
||||
"description": "Tests if one or more sirens are on.",
|
||||
@@ -24,7 +24,7 @@
|
||||
"name": "[%key:component::siren::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "If a siren is on"
|
||||
"name": "Siren is on"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
"""Provides conditions for switches."""
|
||||
|
||||
from homeassistant.components.input_boolean import DOMAIN as INPUT_BOOLEAN_DOMAIN
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
SWITCH_DOMAIN_SPECS = {DOMAIN: DomainSpec(), INPUT_BOOLEAN_DOMAIN: DomainSpec()}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
"is_off": make_entity_state_condition(SWITCH_DOMAIN_SPECS, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(SWITCH_DOMAIN_SPECS, STATE_ON),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
.condition_common: &condition_common
|
||||
target:
|
||||
entity:
|
||||
domain: switch
|
||||
- domain: switch
|
||||
- domain: input_boolean
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
|
||||
@@ -188,7 +188,7 @@ class OptionsFlowHandler(OptionsFlow):
|
||||
)
|
||||
|
||||
|
||||
class TelgramBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Telegram."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@@ -225,9 +225,9 @@ send_media_group:
|
||||
multiple: true
|
||||
label_field: url
|
||||
description_field: caption
|
||||
translation_key: "media"
|
||||
fields:
|
||||
media_type:
|
||||
label: Media type
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
@@ -237,20 +237,16 @@ send_media_group:
|
||||
- "video"
|
||||
translation_key: "media_type"
|
||||
caption:
|
||||
label: Caption
|
||||
selector:
|
||||
text:
|
||||
url:
|
||||
label: URL
|
||||
selector:
|
||||
text:
|
||||
type: url
|
||||
verify_ssl:
|
||||
label: Verify SSL
|
||||
selector:
|
||||
boolean:
|
||||
authentication:
|
||||
label: Authentication
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
@@ -259,16 +255,13 @@ send_media_group:
|
||||
- "bearer_token"
|
||||
translation_key: "authentication"
|
||||
username:
|
||||
label: Username
|
||||
selector:
|
||||
text:
|
||||
password:
|
||||
label: Password
|
||||
selector:
|
||||
text:
|
||||
type: password
|
||||
file:
|
||||
label: File
|
||||
selector:
|
||||
text:
|
||||
parse_mode:
|
||||
|
||||
@@ -279,6 +279,18 @@
|
||||
"upload_voice": "Uploading voice"
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"fields": {
|
||||
"authentication": "Authentication",
|
||||
"caption": "Caption",
|
||||
"file": "File",
|
||||
"media_type": "Media type",
|
||||
"password": "Password",
|
||||
"url": "URL",
|
||||
"username": "Username",
|
||||
"verify_ssl": "Verify SSL"
|
||||
}
|
||||
},
|
||||
"media_type": {
|
||||
"options": {
|
||||
"animation": "Animation",
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.components.weather import (
|
||||
)
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.automation import NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
EntityNumericalConditionWithUnitBase,
|
||||
@@ -26,16 +26,16 @@ from homeassistant.helpers.condition import (
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
TEMPERATURE_DOMAIN_SPECS = {
|
||||
CLIMATE_DOMAIN: NumericalDomainSpec(
|
||||
CLIMATE_DOMAIN: DomainSpec(
|
||||
value_source=CLIMATE_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
),
|
||||
WATER_HEATER_DOMAIN: NumericalDomainSpec(
|
||||
WATER_HEATER_DOMAIN: DomainSpec(
|
||||
value_source=WATER_HEATER_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
WEATHER_DOMAIN: NumericalDomainSpec(
|
||||
WEATHER_DOMAIN: DomainSpec(
|
||||
value_source=ATTR_WEATHER_TEMPERATURE,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.components.weather import (
|
||||
)
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.automation import NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
|
||||
@@ -28,16 +28,14 @@ from homeassistant.helpers.trigger import (
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
TEMPERATURE_DOMAIN_SPECS = {
|
||||
CLIMATE_DOMAIN: NumericalDomainSpec(
|
||||
CLIMATE_DOMAIN: DomainSpec(
|
||||
value_source=CLIMATE_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
),
|
||||
WATER_HEATER_DOMAIN: NumericalDomainSpec(
|
||||
value_source=WATER_HEATER_ATTR_CURRENT_TEMPERATURE
|
||||
),
|
||||
WEATHER_DOMAIN: NumericalDomainSpec(
|
||||
WATER_HEATER_DOMAIN: DomainSpec(value_source=WATER_HEATER_ATTR_CURRENT_TEMPERATURE),
|
||||
WEATHER_DOMAIN: DomainSpec(
|
||||
value_source=ATTR_WEATHER_TEMPERATURE,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,172 +1,30 @@
|
||||
"""Support for TP-Link LTE modems."""
|
||||
"""The tplink_lte integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import attr
|
||||
import tp_connected
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_RECIPIENT,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "tplink_lte"
|
||||
DATA_KEY = "tplink_lte"
|
||||
|
||||
CONF_NOTIFY = "notify"
|
||||
|
||||
_NOTIFY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_RECIPIENT): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NOTIFY): vol.All(
|
||||
cv.ensure_list, [_NOTIFY_SCHEMA]
|
||||
),
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
{DOMAIN: cv.match_all},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class ModemData:
|
||||
"""Class for modem state."""
|
||||
|
||||
host: str = attr.ib()
|
||||
modem: tp_connected.Modem = attr.ib()
|
||||
|
||||
connected: bool = attr.ib(init=False, default=True)
|
||||
|
||||
|
||||
@attr.s
|
||||
class LTEData:
|
||||
"""Shared state."""
|
||||
|
||||
websession: aiohttp.ClientSession = attr.ib()
|
||||
modem_data: dict[str, ModemData] = attr.ib(init=False, factory=dict)
|
||||
|
||||
def get_modem_data(self, config: dict[str, Any]) -> ModemData | None:
|
||||
"""Get the requested or the only modem_data value."""
|
||||
if CONF_HOST in config:
|
||||
return self.modem_data.get(config[CONF_HOST])
|
||||
if len(self.modem_data) == 1:
|
||||
return next(iter(self.modem_data.values()))
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up TP-Link LTE component."""
|
||||
if DATA_KEY not in hass.data:
|
||||
websession = async_create_clientsession(
|
||||
hass, cookie_jar=aiohttp.CookieJar(unsafe=True)
|
||||
)
|
||||
hass.data[DATA_KEY] = LTEData(websession)
|
||||
|
||||
domain_config = config.get(DOMAIN, [])
|
||||
|
||||
tasks = [_setup_lte(hass, conf) for conf in domain_config]
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
for conf in domain_config:
|
||||
for notify_conf in conf.get(CONF_NOTIFY, []):
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hass, Platform.NOTIFY, DOMAIN, notify_conf, config
|
||||
)
|
||||
)
|
||||
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="integration_removed",
|
||||
translation_placeholders={
|
||||
"ghsa_url": "https://github.com/advisories/GHSA-h95x-26f3-88hr",
|
||||
},
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def _setup_lte(
|
||||
hass: HomeAssistant, lte_config: dict[str, Any], delay: int = 0
|
||||
) -> None:
|
||||
"""Set up a TP-Link LTE modem."""
|
||||
|
||||
host: str = lte_config[CONF_HOST]
|
||||
password: str = lte_config[CONF_PASSWORD]
|
||||
|
||||
lte_data: LTEData = hass.data[DATA_KEY]
|
||||
modem = tp_connected.Modem(hostname=host, websession=lte_data.websession)
|
||||
|
||||
modem_data = ModemData(host, modem)
|
||||
|
||||
try:
|
||||
await _login(hass, modem_data, password)
|
||||
except tp_connected.Error:
|
||||
retry_task = hass.loop.create_task(_retry_login(hass, modem_data, password))
|
||||
|
||||
@callback
|
||||
def cleanup_retry(event: Event) -> None:
|
||||
"""Clean up retry task resources."""
|
||||
if not retry_task.done():
|
||||
retry_task.cancel()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_retry)
|
||||
|
||||
|
||||
async def _login(hass: HomeAssistant, modem_data: ModemData, password: str) -> None:
|
||||
"""Log in and complete setup."""
|
||||
await modem_data.modem.login(password=password)
|
||||
modem_data.connected = True
|
||||
lte_data: LTEData = hass.data[DATA_KEY]
|
||||
lte_data.modem_data[modem_data.host] = modem_data
|
||||
|
||||
async def cleanup(event: Event) -> None:
|
||||
"""Clean up resources."""
|
||||
await modem_data.modem.logout()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)
|
||||
|
||||
|
||||
async def _retry_login(
|
||||
hass: HomeAssistant, modem_data: ModemData, password: str
|
||||
) -> None:
|
||||
"""Sleep and retry setup."""
|
||||
|
||||
_LOGGER.warning("Could not connect to %s. Will keep trying", modem_data.host)
|
||||
|
||||
modem_data.connected = False
|
||||
delay = 15
|
||||
|
||||
while not modem_data.connected:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
try:
|
||||
await _login(hass, modem_data, password)
|
||||
_LOGGER.warning("Connected to %s", modem_data.host)
|
||||
except tp_connected.Error:
|
||||
delay = min(2 * delay, 300)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/tplink_lte",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["tp_connected"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["tp-connected==0.0.4"]
|
||||
"requirements": []
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
"""Support for TP-Link LTE notifications."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import attr
|
||||
import tp_connected
|
||||
|
||||
from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
|
||||
from homeassistant.const import CONF_RECIPIENT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DATA_KEY, LTEData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_get_service(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> TplinkNotifyService | None:
|
||||
"""Get the notification service."""
|
||||
if discovery_info is None:
|
||||
return None
|
||||
return TplinkNotifyService(hass, discovery_info)
|
||||
|
||||
|
||||
@attr.s
|
||||
class TplinkNotifyService(BaseNotificationService):
|
||||
"""Implementation of a notification service."""
|
||||
|
||||
hass: HomeAssistant = attr.ib()
|
||||
config: dict[str, Any] = attr.ib()
|
||||
|
||||
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to a user."""
|
||||
|
||||
lte_data: LTEData = self.hass.data[DATA_KEY]
|
||||
modem_data = lte_data.get_modem_data(self.config)
|
||||
if not modem_data:
|
||||
_LOGGER.error("No modem available")
|
||||
return
|
||||
|
||||
phone = self.config[CONF_RECIPIENT]
|
||||
targets = kwargs.get(ATTR_TARGET, phone)
|
||||
if targets and message:
|
||||
for target in targets:
|
||||
try:
|
||||
await modem_data.modem.sms(target, message)
|
||||
except tp_connected.Error:
|
||||
_LOGGER.error("Unable to send to %s", target)
|
||||
8
homeassistant/components/tplink_lte/strings.json
Normal file
8
homeassistant/components/tplink_lte/strings.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"issues": {
|
||||
"integration_removed": {
|
||||
"description": "The TP-Link LTE integration has been removed from Home Assistant.\n\nThe integration has not been working since Home Assistant 2023.6.0, has no maintainer, and its underlying library depends on a package with a [critical security vulnerability]({ghsa_url}).\n\nTo resolve this issue, remove the `tplink_lte` configuration from your `configuration.yaml` file and restart Home Assistant.",
|
||||
"title": "The TP-Link LTE integration has been removed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
|
||||
Condition,
|
||||
@@ -73,7 +73,7 @@ class WaterHeaterTargetTemperatureCondition(EntityNumericalConditionWithUnitBase
|
||||
"""Condition for water heater target temperature."""
|
||||
|
||||
_base_unit = UnitOfTemperature.CELSIUS
|
||||
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_unit_converter = TemperatureConverter
|
||||
|
||||
def _get_entity_unit(self, entity_state: State) -> str | None:
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||
@@ -57,7 +57,7 @@ class _WaterHeaterTargetTemperatureTriggerMixin(
|
||||
"""Mixin for water heater target temperature triggers with unit conversion."""
|
||||
|
||||
_base_unit = UnitOfTemperature.CELSIUS
|
||||
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_unit_converter = TemperatureConverter
|
||||
|
||||
def _get_entity_unit(self, state: State) -> str | None:
|
||||
|
||||
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 4
|
||||
PATCH_VERSION: Final = "0b2"
|
||||
PATCH_VERSION: Final = "0b4"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Helpers for automation."""
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Final, Self
|
||||
@@ -37,14 +37,6 @@ class DomainSpec:
|
||||
"""Attribute name to extract the value from, or None for state.state."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class NumericalDomainSpec(DomainSpec):
|
||||
"""DomainSpec with an optional value converter for numerical triggers."""
|
||||
|
||||
value_converter: Callable[[float], float] | None = None
|
||||
"""Optional converter for numerical values (e.g. uint8 → percentage)."""
|
||||
|
||||
|
||||
def filter_by_domain_specs(
|
||||
hass: HomeAssistant,
|
||||
domain_specs: Mapping[str, DomainSpec],
|
||||
|
||||
@@ -342,10 +342,10 @@ ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
class EntityConditionBase[DomainSpecT: DomainSpec = DomainSpec](Condition):
|
||||
class EntityConditionBase(Condition):
|
||||
"""Base class for entity conditions."""
|
||||
|
||||
_domain_specs: Mapping[str, DomainSpecT]
|
||||
_domain_specs: Mapping[str, DomainSpec]
|
||||
_schema: vol.Schema = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL
|
||||
|
||||
@override
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterable, Mapping
|
||||
from datetime import datetime
|
||||
@@ -771,6 +772,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
devices: ActiveDeviceRegistryItems
|
||||
deleted_devices: DeviceRegistryItems[DeletedDeviceEntry]
|
||||
_device_data: dict[str, DeviceEntry]
|
||||
_loaded_event: asyncio.Event | None = None
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the device registry."""
|
||||
@@ -784,6 +786,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
serialize_in_event_loop=False,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_setup(self) -> None:
|
||||
"""Set up the registry."""
|
||||
self._loaded_event = asyncio.Event()
|
||||
|
||||
@callback
|
||||
def async_get(self, device_id: str) -> DeviceEntry | None:
|
||||
"""Get device.
|
||||
@@ -1463,6 +1470,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the device registry."""
|
||||
assert self._loaded_event is not None
|
||||
assert not self._loaded_event.is_set()
|
||||
|
||||
async_setup_cleanup(self.hass, self)
|
||||
|
||||
data = await self._store.async_load()
|
||||
@@ -1560,6 +1570,16 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
self.deleted_devices = deleted_devices
|
||||
self._device_data = devices.data
|
||||
|
||||
self._loaded_event.set()
|
||||
|
||||
async def async_wait_loaded(self) -> None:
|
||||
"""Wait until the device registry is fully loaded.
|
||||
|
||||
Will only wait if the registry had already been set up.
|
||||
"""
|
||||
if self._loaded_event is not None:
|
||||
await self._loaded_event.wait()
|
||||
|
||||
@callback
|
||||
def _data_to_save(self) -> dict[str, Any]:
|
||||
"""Return data of device registry to store in a file."""
|
||||
@@ -1706,9 +1726,14 @@ def async_get(hass: HomeAssistant) -> DeviceRegistry:
|
||||
return DeviceRegistry(hass)
|
||||
|
||||
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up device registry."""
|
||||
assert DATA_REGISTRY not in hass.data
|
||||
async_get(hass).async_setup()
|
||||
|
||||
|
||||
async def async_load(hass: HomeAssistant, *, load_empty: bool = False) -> None:
|
||||
"""Load device registry."""
|
||||
assert DATA_REGISTRY not in hass.data
|
||||
await async_get(hass).async_load(load_empty=load_empty)
|
||||
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = Event
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STORAGE_VERSION_MAJOR = 1
|
||||
STORAGE_VERSION_MINOR = 21
|
||||
STORAGE_VERSION_MINOR = 22
|
||||
STORAGE_KEY = "core.entity_registry"
|
||||
|
||||
CLEANUP_INTERVAL = 3600 * 24
|
||||
@@ -240,7 +240,6 @@ class RegistryEntry:
|
||||
|
||||
# For backwards compatibility, should be removed in the future
|
||||
compat_aliases: list[str] = attr.ib(factory=list, eq=False)
|
||||
compat_name: str | None = attr.ib(default=None, eq=False)
|
||||
|
||||
# original_name_unprefixed is used to store the result of stripping
|
||||
# the device name prefix from the original_name, if possible.
|
||||
@@ -413,8 +412,7 @@ class RegistryEntry:
|
||||
"has_entity_name": self.has_entity_name,
|
||||
"labels": list(self.labels),
|
||||
"modified_at": self.modified_at,
|
||||
"name": self.compat_name,
|
||||
"name_v2": self.name,
|
||||
"name": self.name,
|
||||
"object_id_base": self.object_id_base,
|
||||
"options": self.options,
|
||||
"original_device_class": self.original_device_class,
|
||||
@@ -471,6 +469,7 @@ def _async_get_full_entity_name(
|
||||
original_name: str | None,
|
||||
original_name_unprefixed: str | None | UndefinedType = UNDEFINED,
|
||||
overridden_name: str | None = None,
|
||||
use_legacy_naming: bool = False,
|
||||
) -> str:
|
||||
"""Get full name for an entity.
|
||||
|
||||
@@ -480,7 +479,7 @@ def _async_get_full_entity_name(
|
||||
if name is None and overridden_name is not None:
|
||||
name = overridden_name
|
||||
|
||||
else:
|
||||
elif not use_legacy_naming or name is None:
|
||||
device_name: str | None = None
|
||||
if (
|
||||
device_id is not None
|
||||
@@ -533,6 +532,7 @@ def async_get_full_entity_name(
|
||||
name=entry.name,
|
||||
original_name=original_name,
|
||||
original_name_unprefixed=original_name_unprefixed,
|
||||
use_legacy_naming=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -660,7 +660,6 @@ class DeletedRegistryEntry:
|
||||
|
||||
# For backwards compatibility, should be removed in the future
|
||||
compat_aliases: list[str] = attr.ib(factory=list, eq=False)
|
||||
compat_name: str | None = attr.ib(default=None, eq=False)
|
||||
|
||||
_cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False)
|
||||
|
||||
@@ -696,8 +695,7 @@ class DeletedRegistryEntry:
|
||||
"id": self.id,
|
||||
"labels": list(self.labels),
|
||||
"modified_at": self.modified_at,
|
||||
"name": self.compat_name,
|
||||
"name_v2": self.name,
|
||||
"name": self.name,
|
||||
"options": self.options if self.options is not UNDEFINED else {},
|
||||
"options_undefined": self.options is UNDEFINED,
|
||||
"orphaned_timestamp": self.orphaned_timestamp,
|
||||
@@ -850,46 +848,37 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
|
||||
for entity in data["entities"]:
|
||||
entity["object_id_base"] = entity["original_name"]
|
||||
|
||||
if old_minor_version < 21:
|
||||
# Version 1.21 migrates the full name to include device name,
|
||||
# even if entity name is overwritten by user.
|
||||
# It also adds support for COMPUTED_NAME in aliases and starts preserving their order.
|
||||
# To avoid a major version bump, we keep the old name and aliases as-is
|
||||
# and use new name_v2 and aliases_v2 fields instead.
|
||||
if old_minor_version == 21:
|
||||
# Version 1.21 has been reverted.
|
||||
# It migrated entity names to the new format stored in `name_v2`
|
||||
# field, automatically stripping any device name prefix present.
|
||||
# The old name was stored in `name` field for backwards compatibility.
|
||||
# For users who already migrated to v1.21, we restore old names
|
||||
# but try to preserve any user renames made since that migration.
|
||||
device_registry = dr.async_get(self.hass)
|
||||
|
||||
for entity in data["entities"]:
|
||||
alias_to_add: str | None = None
|
||||
old_name = entity["name"]
|
||||
name = entity.pop("name_v2")
|
||||
if (
|
||||
(name := entity["name"])
|
||||
(name != old_name)
|
||||
and (device_id := entity["device_id"]) is not None
|
||||
and (device := device_registry.async_get(device_id)) is not None
|
||||
and (device_name := device.name_by_user or device.name)
|
||||
):
|
||||
# Strip the device name prefix from the entity name if present,
|
||||
# and add the full generated name as an alias.
|
||||
# If the name doesn't have the device name prefix and the
|
||||
# entity is exposed to a voice assistant, add the previous
|
||||
# name as an alias instead to preserve backwards compatibility.
|
||||
if (
|
||||
new_name := _async_strip_prefix_from_entity_name(
|
||||
name, device_name
|
||||
)
|
||||
) is not None:
|
||||
name = new_name
|
||||
elif any(
|
||||
entity.get("options", {}).get(key, {}).get("should_expose")
|
||||
for key in ("conversation", "cloud.google_assistant")
|
||||
):
|
||||
alias_to_add = name
|
||||
name = f"{device_name} {name}"
|
||||
|
||||
entity["name_v2"] = name
|
||||
entity["aliases_v2"] = [alias_to_add, *entity["aliases"]]
|
||||
entity["name"] = name
|
||||
|
||||
if old_minor_version < 22:
|
||||
# Version 1.22 adds support for COMPUTED_NAME in aliases and starts preserving
|
||||
# their order.
|
||||
# To avoid a major version bump, we keep the old aliases as-is and use aliases_v2
|
||||
# field instead.
|
||||
for entity in data["entities"]:
|
||||
entity["aliases_v2"] = [None, *entity["aliases"]]
|
||||
|
||||
for entity in data["deleted_entities"]:
|
||||
# We don't know what the device name was, so the only thing we can do
|
||||
# is to clear the overwritten name to not mislead users.
|
||||
entity["name_v2"] = None
|
||||
entity["aliases_v2"] = [None, *entity["aliases"]]
|
||||
|
||||
if old_major_version > 1:
|
||||
@@ -1363,7 +1352,6 @@ class EntityRegistry(BaseRegistry):
|
||||
area_id = deleted_entity.area_id
|
||||
categories = deleted_entity.categories
|
||||
compat_aliases = deleted_entity.compat_aliases
|
||||
compat_name = deleted_entity.compat_name
|
||||
created_at = deleted_entity.created_at
|
||||
device_class = deleted_entity.device_class
|
||||
if deleted_entity.disabled_by is not UNDEFINED:
|
||||
@@ -1395,7 +1383,6 @@ class EntityRegistry(BaseRegistry):
|
||||
area_id = None
|
||||
categories = {}
|
||||
compat_aliases = []
|
||||
compat_name = None
|
||||
device_class = None
|
||||
icon = None
|
||||
labels = set()
|
||||
@@ -1443,7 +1430,6 @@ class EntityRegistry(BaseRegistry):
|
||||
categories=categories,
|
||||
capabilities=none_if_undefined(capabilities),
|
||||
compat_aliases=compat_aliases,
|
||||
compat_name=compat_name,
|
||||
config_entry_id=none_if_undefined(config_entry_id),
|
||||
config_subentry_id=none_if_undefined(config_subentry_id),
|
||||
created_at=created_at,
|
||||
@@ -1506,7 +1492,6 @@ class EntityRegistry(BaseRegistry):
|
||||
area_id=entity.area_id,
|
||||
categories=entity.categories,
|
||||
compat_aliases=entity.compat_aliases,
|
||||
compat_name=entity.compat_name,
|
||||
config_entry_id=config_entry_id,
|
||||
config_subentry_id=entity.config_subentry_id,
|
||||
created_at=entity.created_at,
|
||||
@@ -1620,14 +1605,27 @@ class EntityRegistry(BaseRegistry):
|
||||
for entity in entities:
|
||||
if entity.has_entity_name:
|
||||
continue
|
||||
name = (
|
||||
entity.original_name_unprefixed
|
||||
if by_user and entity.name is None
|
||||
else UNDEFINED
|
||||
)
|
||||
|
||||
# When a user renames a device, update entity names to reflect
|
||||
# the new device name.
|
||||
# An empty name_unprefixed means the entity name equals
|
||||
# the device name (e.g. a main sensor); a non-empty one
|
||||
# is appended as a suffix.
|
||||
name: str | None | UndefinedType = UNDEFINED
|
||||
if (
|
||||
by_user
|
||||
and entity.name is None
|
||||
and (name_unprefixed := entity.original_name_unprefixed) is not None
|
||||
):
|
||||
if not name_unprefixed:
|
||||
name = device_name
|
||||
elif device_name:
|
||||
name = f"{device_name} {name_unprefixed}"
|
||||
|
||||
original_name_unprefixed = _async_strip_prefix_from_entity_name(
|
||||
entity.original_name, device_name
|
||||
)
|
||||
|
||||
self._async_update_entity(
|
||||
entity.entity_id,
|
||||
name=name,
|
||||
@@ -1944,6 +1942,10 @@ class EntityRegistry(BaseRegistry):
|
||||
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the entity registry."""
|
||||
# Device registry must be loaded before entity registry because
|
||||
# migration and entity processing reference device names.
|
||||
await dr.async_get(self.hass).async_wait_loaded()
|
||||
|
||||
_async_setup_cleanup(self.hass, self)
|
||||
_async_setup_entity_restore(self.hass, self)
|
||||
|
||||
@@ -1991,7 +1993,6 @@ class EntityRegistry(BaseRegistry):
|
||||
categories=entity["categories"],
|
||||
capabilities=entity["capabilities"],
|
||||
compat_aliases=entity["aliases"],
|
||||
compat_name=entity["name"],
|
||||
config_entry_id=entity["config_entry_id"],
|
||||
config_subentry_id=entity["config_subentry_id"],
|
||||
created_at=datetime.fromisoformat(entity["created_at"]),
|
||||
@@ -2012,7 +2013,7 @@ class EntityRegistry(BaseRegistry):
|
||||
has_entity_name=entity["has_entity_name"],
|
||||
labels=set(entity["labels"]),
|
||||
modified_at=datetime.fromisoformat(entity["modified_at"]),
|
||||
name=entity["name_v2"],
|
||||
name=entity["name"],
|
||||
object_id_base=entity.get("object_id_base"),
|
||||
options=entity["options"],
|
||||
original_device_class=entity["original_device_class"],
|
||||
@@ -2063,7 +2064,6 @@ class EntityRegistry(BaseRegistry):
|
||||
area_id=entity["area_id"],
|
||||
categories=entity["categories"],
|
||||
compat_aliases=entity["aliases"],
|
||||
compat_name=entity["name"],
|
||||
config_entry_id=entity["config_entry_id"],
|
||||
config_subentry_id=entity["config_subentry_id"],
|
||||
created_at=datetime.fromisoformat(entity["created_at"]),
|
||||
@@ -2083,7 +2083,7 @@ class EntityRegistry(BaseRegistry):
|
||||
id=entity["id"],
|
||||
labels=set(entity["labels"]),
|
||||
modified_at=datetime.fromisoformat(entity["modified_at"]),
|
||||
name=entity["name_v2"],
|
||||
name=entity["name"],
|
||||
options=entity["options"]
|
||||
if not entity["options_undefined"]
|
||||
else UNDEFINED,
|
||||
|
||||
@@ -68,7 +68,6 @@ from homeassistant.util.yaml import load_yaml_dict
|
||||
from . import config_validation as cv, selector
|
||||
from .automation import (
|
||||
DomainSpec,
|
||||
NumericalDomainSpec,
|
||||
ThresholdConfig,
|
||||
filter_by_domain_specs,
|
||||
get_absolute_description_key,
|
||||
@@ -340,10 +339,10 @@ ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
class EntityTriggerBase[DomainSpecT: DomainSpec = DomainSpec](Trigger):
|
||||
class EntityTriggerBase(Trigger):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
_domain_specs: Mapping[str, DomainSpecT]
|
||||
_domain_specs: Mapping[str, DomainSpec]
|
||||
_schema: vol.Schema = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST
|
||||
|
||||
@override
|
||||
@@ -534,7 +533,7 @@ NUMERICAL_ATTRIBUTE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
class EntityNumericalStateTriggerBase(EntityTriggerBase[NumericalDomainSpec]):
|
||||
class EntityNumericalStateTriggerBase(EntityTriggerBase):
|
||||
"""Base class for numerical state and state attribute triggers."""
|
||||
|
||||
_valid_unit: str | None | UndefinedType = UNDEFINED
|
||||
@@ -595,21 +594,12 @@ class EntityNumericalStateTriggerBase(EntityTriggerBase[NumericalDomainSpec]):
|
||||
# Entity state is not a valid number
|
||||
return None
|
||||
|
||||
def _get_converter(self, state: State) -> Callable[[float], float]:
|
||||
"""Get the value converter for an entity."""
|
||||
domain_spec = self._domain_specs[state.domain]
|
||||
if domain_spec.value_converter is not None:
|
||||
return domain_spec.value_converter
|
||||
return lambda x: x
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state or state attribute matches the expected one."""
|
||||
# Handle missing or None value case first to avoid expensive exceptions
|
||||
if (_attribute_value := self._get_tracked_value(state)) is None:
|
||||
if (current_value := self._get_tracked_value(state)) is None:
|
||||
return False
|
||||
|
||||
current_value = self._get_converter(state)(_attribute_value)
|
||||
|
||||
if self._threshold_type == NumericThresholdType.ANY:
|
||||
# If the threshold type is "any" we always trigger on valid state
|
||||
# changes
|
||||
@@ -890,7 +880,7 @@ def make_entity_origin_state_trigger(
|
||||
|
||||
|
||||
def make_entity_numerical_state_changed_trigger(
|
||||
domain_specs: Mapping[str, NumericalDomainSpec],
|
||||
domain_specs: Mapping[str, DomainSpec],
|
||||
valid_unit: str | None | UndefinedType = UNDEFINED,
|
||||
) -> type[EntityNumericalStateChangedTriggerBase]:
|
||||
"""Create a trigger for numerical state value change."""
|
||||
@@ -905,7 +895,7 @@ def make_entity_numerical_state_changed_trigger(
|
||||
|
||||
|
||||
def make_entity_numerical_state_crossed_threshold_trigger(
|
||||
domain_specs: Mapping[str, NumericalDomainSpec],
|
||||
domain_specs: Mapping[str, DomainSpec],
|
||||
valid_unit: str | None | UndefinedType = UNDEFINED,
|
||||
) -> type[EntityNumericalStateCrossedThresholdTriggerBase]:
|
||||
"""Create a trigger for numerical state value crossing a threshold."""
|
||||
@@ -920,7 +910,7 @@ def make_entity_numerical_state_crossed_threshold_trigger(
|
||||
|
||||
|
||||
def make_entity_numerical_state_changed_with_unit_trigger(
|
||||
domain_specs: Mapping[str, NumericalDomainSpec],
|
||||
domain_specs: Mapping[str, DomainSpec],
|
||||
base_unit: str,
|
||||
unit_converter: type[BaseUnitConverter],
|
||||
) -> type[EntityNumericalStateChangedTriggerWithUnitBase]:
|
||||
@@ -937,7 +927,7 @@ def make_entity_numerical_state_changed_with_unit_trigger(
|
||||
|
||||
|
||||
def make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
domain_specs: Mapping[str, NumericalDomainSpec],
|
||||
domain_specs: Mapping[str, DomainSpec],
|
||||
base_unit: str,
|
||||
unit_converter: type[BaseUnitConverter],
|
||||
) -> type[EntityNumericalStateCrossedThresholdTriggerWithUnitBase]:
|
||||
|
||||
@@ -39,7 +39,7 @@ habluetooth==5.11.1
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20260325.0
|
||||
home-assistant-frontend==20260325.2
|
||||
home-assistant-intents==2026.3.24
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
@@ -55,6 +55,7 @@ def run(args: Sequence[str] | None) -> None:
|
||||
async def run_command(args: argparse.Namespace) -> None:
|
||||
"""Run the command."""
|
||||
hass = HomeAssistant(os.path.join(os.getcwd(), args.config))
|
||||
dr.async_setup(hass)
|
||||
await asyncio.gather(dr.async_load(hass), er.async_load(hass))
|
||||
hass.auth = await auth_manager_from_config(hass, [{"type": "homeassistant"}], [])
|
||||
provider = hass.auth.auth_providers[0]
|
||||
|
||||
@@ -302,6 +302,7 @@ async def async_check_config(config_dir):
|
||||
hass = core.HomeAssistant(config_dir)
|
||||
loader.async_setup(hass)
|
||||
hass.config_entries = ConfigEntries(hass, {})
|
||||
dr.async_setup(hass)
|
||||
await ar.async_load(hass)
|
||||
await dr.async_load(hass)
|
||||
await er.async_load(hass)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.4.0b2"
|
||||
version = "2026.4.0b4"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
|
||||
15
requirements_all.txt
generated
15
requirements_all.txt
generated
@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==13.3.0
|
||||
aioamazondevices==13.3.1
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -1229,7 +1229,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260325.0
|
||||
home-assistant-frontend==20260325.2
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.3.24
|
||||
@@ -1286,7 +1286,7 @@ icalendar==6.3.1
|
||||
icmplib==3.0
|
||||
|
||||
# homeassistant.components.idasen_desk
|
||||
idasen-ha==2.6.4
|
||||
idasen-ha==2.6.5
|
||||
|
||||
# homeassistant.components.idrive_e2
|
||||
idrive-e2-client==0.1.1
|
||||
@@ -1452,7 +1452,7 @@ livisi==0.0.25
|
||||
locationsharinglib==5.0.1
|
||||
|
||||
# homeassistant.components.lojack
|
||||
lojack-api==0.7.1
|
||||
lojack-api==0.7.2
|
||||
|
||||
# homeassistant.components.london_underground
|
||||
london-tube-status==0.5
|
||||
@@ -2651,7 +2651,7 @@ python-overseerr==0.9.0
|
||||
python-picnic-api2==1.3.1
|
||||
|
||||
# homeassistant.components.pooldose
|
||||
python-pooldose==0.8.6
|
||||
python-pooldose==0.9.0
|
||||
|
||||
# homeassistant.components.hr_energy_qube
|
||||
python-qube-heatpump==1.7.0
|
||||
@@ -2823,7 +2823,7 @@ refoss-ha==1.2.5
|
||||
regenmaschine==2024.03.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.5.6
|
||||
renault-api==0.5.7
|
||||
|
||||
# homeassistant.components.renson
|
||||
renson-endura-delta==1.7.2
|
||||
@@ -3138,9 +3138,6 @@ toonapi==0.3.0
|
||||
# homeassistant.components.totalconnect
|
||||
total-connect-client==2025.12.2
|
||||
|
||||
# homeassistant.components.tplink_lte
|
||||
tp-connected==0.0.4
|
||||
|
||||
# homeassistant.components.tplink_omada
|
||||
tplink-omada-client==1.5.6
|
||||
|
||||
|
||||
12
requirements_test_all.txt
generated
12
requirements_test_all.txt
generated
@@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==13.3.0
|
||||
aioamazondevices==13.3.1
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -1093,7 +1093,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260325.0
|
||||
home-assistant-frontend==20260325.2
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.3.24
|
||||
@@ -1144,7 +1144,7 @@ icalendar==6.3.1
|
||||
icmplib==3.0
|
||||
|
||||
# homeassistant.components.idasen_desk
|
||||
idasen-ha==2.6.4
|
||||
idasen-ha==2.6.5
|
||||
|
||||
# homeassistant.components.idrive_e2
|
||||
idrive-e2-client==0.1.1
|
||||
@@ -1274,7 +1274,7 @@ libsoundtouch==0.8
|
||||
livisi==0.0.25
|
||||
|
||||
# homeassistant.components.lojack
|
||||
lojack-api==0.7.1
|
||||
lojack-api==0.7.2
|
||||
|
||||
# homeassistant.components.london_underground
|
||||
london-tube-status==0.5
|
||||
@@ -2253,7 +2253,7 @@ python-overseerr==0.9.0
|
||||
python-picnic-api2==1.3.1
|
||||
|
||||
# homeassistant.components.pooldose
|
||||
python-pooldose==0.8.6
|
||||
python-pooldose==0.9.0
|
||||
|
||||
# homeassistant.components.hr_energy_qube
|
||||
python-qube-heatpump==1.7.0
|
||||
@@ -2401,7 +2401,7 @@ refoss-ha==1.2.5
|
||||
regenmaschine==2024.03.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.5.6
|
||||
renault-api==0.5.7
|
||||
|
||||
# homeassistant.components.renson
|
||||
renson-endura-delta==1.7.2
|
||||
|
||||
@@ -305,6 +305,8 @@ async def async_test_home_assistant(
|
||||
hass
|
||||
)
|
||||
if load_registries:
|
||||
dr.async_setup(hass)
|
||||
|
||||
with (
|
||||
patch.object(StoreWithoutWriteLoad, "async_load", return_value=None),
|
||||
patch(
|
||||
|
||||
@@ -40,12 +40,6 @@ async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
return await target_entities(hass, "sensor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_numbers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple number entities associated with different targets."""
|
||||
return await target_entities(hass, "number")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"condition",
|
||||
[
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.number import NumberDeviceClass
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
@@ -40,12 +39,6 @@ async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
return await target_entities(hass, "sensor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_numbers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple number entities associated with different targets."""
|
||||
return await target_entities(hass, "number")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_key",
|
||||
[
|
||||
@@ -364,122 +357,3 @@ async def test_battery_level_crossed_threshold_sensor_behavior_last(
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_changed_trigger_states(
|
||||
"battery.level_changed",
|
||||
device_class=NumberDeviceClass.BATTERY,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: "%"},
|
||||
),
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"battery.level_crossed_threshold",
|
||||
device_class=NumberDeviceClass.BATTERY,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: "%"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_battery_number_trigger_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test battery number triggers with 'any' behavior."""
|
||||
await assert_trigger_behavior_any(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"battery.level_crossed_threshold",
|
||||
device_class=NumberDeviceClass.BATTERY,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: "%"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_battery_level_crossed_threshold_number_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test battery level_crossed_threshold trigger fires on the first number state change."""
|
||||
await assert_trigger_behavior_first(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"battery.level_crossed_threshold",
|
||||
device_class=NumberDeviceClass.BATTERY,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: "%"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_battery_level_crossed_threshold_number_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test battery level_crossed_threshold trigger fires when the last number changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
@@ -47,6 +47,7 @@ async def target_climates(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"climate.is_cooling",
|
||||
"climate.is_drying",
|
||||
"climate.is_heating",
|
||||
"climate.is_hvac_mode",
|
||||
"climate.target_humidity",
|
||||
"climate.target_temperature",
|
||||
],
|
||||
@@ -83,6 +84,24 @@ async def test_climate_conditions_gated_by_labs_flag(
|
||||
],
|
||||
other_states=[HVACMode.OFF],
|
||||
),
|
||||
*(
|
||||
param
|
||||
for mode in HVACMode
|
||||
for param in parametrize_condition_states_any(
|
||||
condition="climate.is_hvac_mode",
|
||||
condition_options={"hvac_mode": [mode]},
|
||||
target_states=[mode],
|
||||
other_states=[m for m in HVACMode if m != mode],
|
||||
)
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
condition="climate.is_hvac_mode",
|
||||
condition_options={"hvac_mode": [HVACMode.HEAT, HVACMode.COOL]},
|
||||
target_states=[HVACMode.HEAT, HVACMode.COOL],
|
||||
other_states=[
|
||||
m for m in HVACMode if m not in (HVACMode.HEAT, HVACMode.COOL)
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_climate_state_condition_behavior_any(
|
||||
@@ -133,6 +152,24 @@ async def test_climate_state_condition_behavior_any(
|
||||
],
|
||||
other_states=[HVACMode.OFF],
|
||||
),
|
||||
*(
|
||||
param
|
||||
for mode in HVACMode
|
||||
for param in parametrize_condition_states_all(
|
||||
condition="climate.is_hvac_mode",
|
||||
condition_options={"hvac_mode": [mode]},
|
||||
target_states=[mode],
|
||||
other_states=[m for m in HVACMode if m != mode],
|
||||
)
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
condition="climate.is_hvac_mode",
|
||||
condition_options={"hvac_mode": [HVACMode.HEAT, HVACMode.COOL]},
|
||||
target_states=[HVACMode.HEAT, HVACMode.COOL],
|
||||
other_states=[
|
||||
m for m in HVACMode if m not in (HVACMode.HEAT, HVACMode.COOL)
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_climate_state_condition_behavior_all(
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
"""Test humidifier conditions."""
|
||||
|
||||
from contextlib import AbstractContextManager, nullcontext as does_not_raise
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.humidifier.condition import CONF_MODE
|
||||
from homeassistant.components.humidifier.const import (
|
||||
ATTR_ACTION,
|
||||
ATTR_HUMIDITY,
|
||||
HumidifierAction,
|
||||
HumidifierEntityFeature,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_MODE,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import async_validate_condition_config
|
||||
|
||||
from tests.components.common import (
|
||||
ConditionStateDescription,
|
||||
@@ -39,6 +52,7 @@ async def target_humidifiers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"humidifier.is_on",
|
||||
"humidifier.is_drying",
|
||||
"humidifier.is_humidifying",
|
||||
"humidifier.is_mode",
|
||||
"humidifier.is_target_humidity",
|
||||
],
|
||||
)
|
||||
@@ -153,6 +167,20 @@ async def test_humidifier_state_condition_behavior_all(
|
||||
target_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.HUMIDIFYING})],
|
||||
other_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.IDLE})],
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
condition="humidifier.is_mode",
|
||||
condition_options={CONF_MODE: ["eco", "sleep"]},
|
||||
target_states=[
|
||||
(STATE_ON, {ATTR_MODE: "eco"}),
|
||||
(STATE_ON, {ATTR_MODE: "sleep"}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {ATTR_MODE: "normal"}),
|
||||
],
|
||||
required_filter_attributes={
|
||||
ATTR_SUPPORTED_FEATURES: HumidifierEntityFeature.MODES
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_humidifier_attribute_condition_behavior_any(
|
||||
@@ -196,6 +224,20 @@ async def test_humidifier_attribute_condition_behavior_any(
|
||||
target_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.HUMIDIFYING})],
|
||||
other_states=[(STATE_ON, {ATTR_ACTION: HumidifierAction.IDLE})],
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
condition="humidifier.is_mode",
|
||||
condition_options={CONF_MODE: ["eco", "sleep"]},
|
||||
target_states=[
|
||||
(STATE_ON, {ATTR_MODE: "eco"}),
|
||||
(STATE_ON, {ATTR_MODE: "sleep"}),
|
||||
],
|
||||
other_states=[
|
||||
(STATE_ON, {ATTR_MODE: "normal"}),
|
||||
],
|
||||
required_filter_attributes={
|
||||
ATTR_SUPPORTED_FEATURES: HumidifierEntityFeature.MODES
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_humidifier_attribute_condition_behavior_all(
|
||||
@@ -291,3 +333,51 @@ async def test_humidifier_numerical_condition_behavior_all(
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "expected_result"),
|
||||
[
|
||||
# Valid configurations
|
||||
(
|
||||
"humidifier.is_mode",
|
||||
{CONF_MODE: ["eco", "sleep"]},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"humidifier.is_mode",
|
||||
{CONF_MODE: "eco"},
|
||||
does_not_raise(),
|
||||
),
|
||||
# Invalid configurations
|
||||
(
|
||||
"humidifier.is_mode",
|
||||
# Empty mode list
|
||||
{CONF_MODE: []},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
"humidifier.is_mode",
|
||||
# Missing CONF_MODE
|
||||
{},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_humidifier_is_mode_condition_validation(
|
||||
hass: HomeAssistant,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
expected_result: AbstractContextManager,
|
||||
) -> None:
|
||||
"""Test humidifier is_mode condition config validation."""
|
||||
with expected_result:
|
||||
await async_validate_condition_config(
|
||||
hass,
|
||||
{
|
||||
"condition": condition,
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "humidifier.test"},
|
||||
CONF_OPTIONS: condition_options,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.components.humidifier import (
|
||||
ATTR_CURRENT_HUMIDITY as HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
|
||||
)
|
||||
from homeassistant.components.weather import ATTR_WEATHER_HUMIDITY
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -36,12 +37,6 @@ async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
return await target_entities(hass, "sensor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_numbers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple number entities associated with different targets."""
|
||||
return await target_entities(hass, "number")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_climates(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple climate entities associated with different targets."""
|
||||
@@ -54,6 +49,12 @@ async def target_humidifiers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
return await target_entities(hass, "humidifier")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_weathers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple weather entities associated with different targets."""
|
||||
return await target_entities(hass, "weather")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"condition",
|
||||
[
|
||||
@@ -139,78 +140,6 @@ async def test_humidity_sensor_condition_behavior_all(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
parametrize_numerical_condition_above_below_any(
|
||||
"humidity.is_value",
|
||||
device_class="humidity",
|
||||
unit_attributes=_HUMIDITY_UNIT_ATTRS,
|
||||
),
|
||||
)
|
||||
async def test_humidity_number_condition_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the humidity number condition with 'any' behavior."""
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
parametrize_numerical_condition_above_below_all(
|
||||
"humidity.is_value",
|
||||
device_class="humidity",
|
||||
unit_attributes=_HUMIDITY_UNIT_ATTRS,
|
||||
),
|
||||
)
|
||||
async def test_humidity_number_condition_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the humidity number condition with 'all' behavior."""
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
@@ -353,3 +282,75 @@ async def test_humidity_humidifier_condition_behavior_all(
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("weather"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
parametrize_numerical_attribute_condition_above_below_any(
|
||||
"humidity.is_value",
|
||||
"sunny",
|
||||
ATTR_WEATHER_HUMIDITY,
|
||||
),
|
||||
)
|
||||
async def test_humidity_weather_condition_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_weathers: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the humidity weather condition with 'any' behavior."""
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_weathers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("weather"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
parametrize_numerical_attribute_condition_above_below_all(
|
||||
"humidity.is_value",
|
||||
"sunny",
|
||||
ATTR_WEATHER_HUMIDITY,
|
||||
),
|
||||
)
|
||||
async def test_humidity_weather_condition_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_weathers: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the humidity weather condition with 'all' behavior."""
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_weathers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
@@ -40,12 +40,6 @@ async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
return await target_entities(hass, "sensor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_numbers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple number entities associated with different targets."""
|
||||
return await target_entities(hass, "number")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"condition",
|
||||
[
|
||||
@@ -221,75 +215,3 @@ async def test_illuminance_value_condition_behavior_all(
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
parametrize_numerical_condition_above_below_any(
|
||||
"illuminance.is_value",
|
||||
device_class="illuminance",
|
||||
unit_attributes=_ILLUMINANCE_UNIT_ATTRS,
|
||||
),
|
||||
)
|
||||
async def test_illuminance_value_number_condition_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the illuminance value condition with number entities and 'any' behavior."""
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
parametrize_numerical_condition_above_below_all(
|
||||
"illuminance.is_value",
|
||||
device_class="illuminance",
|
||||
unit_attributes=_ILLUMINANCE_UNIT_ATTRS,
|
||||
),
|
||||
)
|
||||
async def test_illuminance_value_number_condition_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the illuminance value condition with number entities and 'all' behavior."""
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Any
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.number import NumberDeviceClass
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
@@ -36,12 +35,6 @@ async def target_binary_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
return await target_entities(hass, "binary_sensor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_numbers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple number entities associated with different targets."""
|
||||
return await target_entities(hass, "number")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple sensor entities associated with different targets."""
|
||||
@@ -340,125 +333,3 @@ async def test_illuminance_trigger_sensor_crossed_threshold_behavior_last(
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
# --- Number changed/crossed_threshold tests ---
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_changed_trigger_states(
|
||||
"illuminance.changed",
|
||||
device_class=NumberDeviceClass.ILLUMINANCE,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
|
||||
),
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"illuminance.crossed_threshold",
|
||||
device_class=NumberDeviceClass.ILLUMINANCE,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_illuminance_trigger_number_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test illuminance trigger fires for number entities with device_class illuminance."""
|
||||
await assert_trigger_behavior_any(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"illuminance.crossed_threshold",
|
||||
device_class=NumberDeviceClass.ILLUMINANCE,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_illuminance_trigger_number_crossed_threshold_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test illuminance crossed_threshold trigger fires on the first number state change."""
|
||||
await assert_trigger_behavior_first(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"illuminance.crossed_threshold",
|
||||
device_class=NumberDeviceClass.ILLUMINANCE,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_illuminance_trigger_number_crossed_threshold_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test illuminance crossed_threshold trigger fires when the last number changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
@@ -4,11 +4,14 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.common import (
|
||||
ConditionStateDescription,
|
||||
assert_condition_behavior_all,
|
||||
assert_condition_behavior_any,
|
||||
assert_condition_gated_by_labs_flag,
|
||||
create_target_condition,
|
||||
parametrize_condition_states_all,
|
||||
@@ -19,6 +22,116 @@ from tests.components.common import (
|
||||
)
|
||||
|
||||
|
||||
def parametrize_brightness_condition_states_any(
|
||||
condition: str, state: str, attribute: str
|
||||
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
|
||||
"""Parametrize above/below threshold test cases for brightness conditions.
|
||||
|
||||
Note: The brightness in the condition configuration is in percentage (0-100) scale,
|
||||
the underlying attribute in the state is in uint8 (0-255) scale.
|
||||
"""
|
||||
return [
|
||||
*parametrize_condition_states_any(
|
||||
condition=condition,
|
||||
condition_options={"threshold": {"type": "above", "value": {"number": 10}}},
|
||||
target_states=[
|
||||
(state, {attribute: 128}),
|
||||
(state, {attribute: 255}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: None}),
|
||||
],
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
condition=condition,
|
||||
condition_options={"threshold": {"type": "below", "value": {"number": 90}}},
|
||||
target_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 128}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: 255}),
|
||||
(state, {attribute: None}),
|
||||
],
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
condition=condition,
|
||||
condition_options={
|
||||
"threshold": {
|
||||
"type": "between",
|
||||
"value_min": {"number": 10},
|
||||
"value_max": {"number": 90},
|
||||
}
|
||||
},
|
||||
target_states=[
|
||||
(state, {attribute: 128}),
|
||||
(state, {attribute: 153}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 255}),
|
||||
(state, {attribute: None}),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def parametrize_brightness_condition_states_all(
|
||||
condition: str, state: str, attribute: str
|
||||
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
|
||||
"""Parametrize above/below threshold test cases for brightness conditions with 'all' behavior.
|
||||
|
||||
Note: The brightness in the condition configuration is in percentage (0-100) scale,
|
||||
the underlying attribute in the state is in uint8 (0-255) scale.
|
||||
"""
|
||||
return [
|
||||
*parametrize_condition_states_all(
|
||||
condition=condition,
|
||||
condition_options={"threshold": {"type": "above", "value": {"number": 10}}},
|
||||
target_states=[
|
||||
(state, {attribute: 128}),
|
||||
(state, {attribute: 255}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: None}),
|
||||
],
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
condition=condition,
|
||||
condition_options={"threshold": {"type": "below", "value": {"number": 90}}},
|
||||
target_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 128}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: 255}),
|
||||
(state, {attribute: None}),
|
||||
],
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
condition=condition,
|
||||
condition_options={
|
||||
"threshold": {
|
||||
"type": "between",
|
||||
"value_min": {"number": 10},
|
||||
"value_max": {"number": 90},
|
||||
}
|
||||
},
|
||||
target_states=[
|
||||
(state, {attribute: 128}),
|
||||
(state, {attribute: 153}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 255}),
|
||||
(state, {attribute: None}),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_lights(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple light entities associated with different targets."""
|
||||
@@ -38,6 +151,7 @@ async def target_switches(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
@pytest.mark.parametrize(
|
||||
"condition",
|
||||
[
|
||||
"light.is_brightness",
|
||||
"light.is_off",
|
||||
"light.is_on",
|
||||
],
|
||||
@@ -176,3 +290,75 @@ async def test_light_state_condition_behavior_all(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert condition(hass) == state["condition_true"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("light"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_brightness_condition_states_any(
|
||||
"light.is_brightness", STATE_ON, ATTR_BRIGHTNESS
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_light_brightness_condition_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_lights: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the light brightness condition with the 'any' behavior."""
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_lights,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("light"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_brightness_condition_states_all(
|
||||
"light.is_brightness", STATE_ON, ATTR_BRIGHTNESS
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_light_brightness_condition_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_lights: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the light brightness condition with the 'all' behavior."""
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_lights,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
@@ -5243,6 +5243,7 @@
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'airfry',
|
||||
'almond_macaroons_1_tray',
|
||||
'almond_macaroons_2_trays',
|
||||
'amaranth',
|
||||
@@ -5294,6 +5295,7 @@
|
||||
'blanching',
|
||||
'blueberry_muffins',
|
||||
'bologna_sausage',
|
||||
'booster',
|
||||
'bottling',
|
||||
'bottling_hard',
|
||||
'bottling_medium',
|
||||
@@ -5856,6 +5858,7 @@
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Oven Program',
|
||||
'options': list([
|
||||
'airfry',
|
||||
'almond_macaroons_1_tray',
|
||||
'almond_macaroons_2_trays',
|
||||
'amaranth',
|
||||
@@ -5907,6 +5910,7 @@
|
||||
'blanching',
|
||||
'blueberry_muffins',
|
||||
'bologna_sausage',
|
||||
'booster',
|
||||
'bottling',
|
||||
'bottling_hard',
|
||||
'bottling_medium',
|
||||
@@ -6449,6 +6453,7 @@
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'cooling_down',
|
||||
'energy_save',
|
||||
'heating_up',
|
||||
'not_running',
|
||||
@@ -6495,6 +6500,7 @@
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Oven Program phase',
|
||||
'options': list([
|
||||
'cooling_down',
|
||||
'energy_save',
|
||||
'heating_up',
|
||||
'not_running',
|
||||
@@ -7624,6 +7630,7 @@
|
||||
'starch',
|
||||
'steam_care',
|
||||
'stuffed_toys',
|
||||
'table_linen',
|
||||
'trainers',
|
||||
'trainers_refresh',
|
||||
'warm_air',
|
||||
@@ -7707,6 +7714,7 @@
|
||||
'starch',
|
||||
'steam_care',
|
||||
'stuffed_toys',
|
||||
'table_linen',
|
||||
'trainers',
|
||||
'trainers_refresh',
|
||||
'warm_air',
|
||||
@@ -9041,6 +9049,7 @@
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'airfry',
|
||||
'almond_macaroons_1_tray',
|
||||
'almond_macaroons_2_trays',
|
||||
'amaranth',
|
||||
@@ -9092,6 +9101,7 @@
|
||||
'blanching',
|
||||
'blueberry_muffins',
|
||||
'bologna_sausage',
|
||||
'booster',
|
||||
'bottling',
|
||||
'bottling_hard',
|
||||
'bottling_medium',
|
||||
@@ -9654,6 +9664,7 @@
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Oven Program',
|
||||
'options': list([
|
||||
'airfry',
|
||||
'almond_macaroons_1_tray',
|
||||
'almond_macaroons_2_trays',
|
||||
'amaranth',
|
||||
@@ -9705,6 +9716,7 @@
|
||||
'blanching',
|
||||
'blueberry_muffins',
|
||||
'bologna_sausage',
|
||||
'booster',
|
||||
'bottling',
|
||||
'bottling_hard',
|
||||
'bottling_medium',
|
||||
@@ -10247,6 +10259,7 @@
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'cooling_down',
|
||||
'energy_save',
|
||||
'heating_up',
|
||||
'not_running',
|
||||
@@ -10293,6 +10306,7 @@
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Oven Program phase',
|
||||
'options': list([
|
||||
'cooling_down',
|
||||
'energy_save',
|
||||
'heating_up',
|
||||
'not_running',
|
||||
@@ -11422,6 +11436,7 @@
|
||||
'starch',
|
||||
'steam_care',
|
||||
'stuffed_toys',
|
||||
'table_linen',
|
||||
'trainers',
|
||||
'trainers_refresh',
|
||||
'warm_air',
|
||||
@@ -11505,6 +11520,7 @@
|
||||
'starch',
|
||||
'steam_care',
|
||||
'stuffed_toys',
|
||||
'table_linen',
|
||||
'trainers',
|
||||
'trainers_refresh',
|
||||
'warm_air',
|
||||
|
||||
@@ -40,12 +40,6 @@ async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
return await target_entities(hass, "sensor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_numbers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple number entities associated with different targets."""
|
||||
return await target_entities(hass, "number")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"condition",
|
||||
[
|
||||
@@ -221,75 +215,3 @@ async def test_moisture_sensor_condition_behavior_all(
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
parametrize_numerical_condition_above_below_any(
|
||||
"moisture.is_value",
|
||||
device_class="moisture",
|
||||
unit_attributes=_MOISTURE_UNIT_ATTRS,
|
||||
),
|
||||
)
|
||||
async def test_moisture_number_condition_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the moisture number condition with 'any' behavior."""
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
parametrize_numerical_condition_above_below_all(
|
||||
"moisture.is_value",
|
||||
device_class="moisture",
|
||||
unit_attributes=_MOISTURE_UNIT_ATTRS,
|
||||
),
|
||||
)
|
||||
async def test_moisture_number_condition_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the moisture number condition with 'all' behavior."""
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Any
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.number import NumberDeviceClass
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
@@ -36,12 +35,6 @@ async def target_binary_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
return await target_entities(hass, "binary_sensor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_numbers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple number entities associated with different targets."""
|
||||
return await target_entities(hass, "number")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple sensor entities associated with different targets."""
|
||||
@@ -336,128 +329,6 @@ async def test_moisture_trigger_sensor_crossed_threshold_behavior_last(
|
||||
)
|
||||
|
||||
|
||||
# --- Number entity tests ---
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_changed_trigger_states(
|
||||
"moisture.changed",
|
||||
device_class=NumberDeviceClass.MOISTURE,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: "%"},
|
||||
),
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"moisture.crossed_threshold",
|
||||
device_class=NumberDeviceClass.MOISTURE,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: "%"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_moisture_trigger_number_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test moisture trigger fires for number entities with device_class moisture."""
|
||||
await assert_trigger_behavior_any(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"moisture.crossed_threshold",
|
||||
device_class=NumberDeviceClass.MOISTURE,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: "%"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_moisture_trigger_number_crossed_threshold_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test moisture crossed_threshold trigger fires on the first number state change."""
|
||||
await assert_trigger_behavior_first(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"moisture.crossed_threshold",
|
||||
device_class=NumberDeviceClass.MOISTURE,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: "%"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_moisture_trigger_number_crossed_threshold_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test moisture crossed_threshold trigger fires when the last number changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "limit_entities"),
|
||||
|
||||
@@ -26,12 +26,6 @@ async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
return await target_entities(hass, "sensor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_numbers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple number entities associated with different targets."""
|
||||
return await target_entities(hass, "number")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"condition",
|
||||
["power.is_value"],
|
||||
@@ -117,80 +111,6 @@ async def test_power_sensor_condition_behavior_all(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
parametrize_numerical_condition_above_below_any(
|
||||
"power.is_value",
|
||||
device_class="power",
|
||||
threshold_unit=UnitOfPower.WATT,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.WATT},
|
||||
),
|
||||
)
|
||||
async def test_power_number_condition_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the power number condition with 'any' behavior."""
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
parametrize_numerical_condition_above_below_all(
|
||||
"power.is_value",
|
||||
device_class="power",
|
||||
threshold_unit=UnitOfPower.WATT,
|
||||
unit_attributes={ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.WATT},
|
||||
),
|
||||
)
|
||||
async def test_power_number_condition_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the power number condition with 'all' behavior."""
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
async def test_power_condition_unit_conversion_sensor(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.number import NumberDeviceClass
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -24,12 +23,6 @@ from tests.components.common import (
|
||||
_POWER_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.WATT}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_numbers(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple number entities associated with different targets."""
|
||||
return await target_entities(hass, "number")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple sensor entities associated with different targets."""
|
||||
@@ -171,129 +164,3 @@ async def test_power_trigger_sensor_crossed_threshold_behavior_last(
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
# --- Number entity tests ---
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_changed_trigger_states(
|
||||
"power.changed",
|
||||
device_class=NumberDeviceClass.POWER,
|
||||
threshold_unit=UnitOfPower.WATT,
|
||||
unit_attributes=_POWER_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"power.crossed_threshold",
|
||||
device_class=NumberDeviceClass.POWER,
|
||||
threshold_unit=UnitOfPower.WATT,
|
||||
unit_attributes=_POWER_UNIT_ATTRIBUTES,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_power_trigger_number_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test power trigger fires for number entities with device_class power."""
|
||||
await assert_trigger_behavior_any(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"power.crossed_threshold",
|
||||
device_class=NumberDeviceClass.POWER,
|
||||
threshold_unit=UnitOfPower.WATT,
|
||||
unit_attributes=_POWER_UNIT_ATTRIBUTES,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_power_trigger_number_crossed_threshold_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test power crossed_threshold trigger fires on the first number state change."""
|
||||
await assert_trigger_behavior_first(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("number"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
|
||||
"power.crossed_threshold",
|
||||
device_class=NumberDeviceClass.POWER,
|
||||
threshold_unit=UnitOfPower.WATT,
|
||||
unit_attributes=_POWER_UNIT_ATTRIBUTES,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_power_trigger_number_crossed_threshold_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
target_numbers: dict[str, list[str]],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[TriggerStateDescription],
|
||||
) -> None:
|
||||
"""Test power crossed_threshold trigger fires when the last number changes state."""
|
||||
await assert_trigger_behavior_last(
|
||||
hass,
|
||||
target_entities=target_numbers,
|
||||
trigger_target_config=trigger_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
trigger=trigger,
|
||||
trigger_options=trigger_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
@@ -31,9 +31,20 @@ POWER_PERMISSIONS = {
|
||||
"/vms/101": {"VM.PowerMgmt": 0},
|
||||
}
|
||||
|
||||
SNAPSHOT_PERMISSIONS = {
|
||||
"/vms": {"VM.Snapshot": 1},
|
||||
"/vms/101": {"VM.Snapshot": 0},
|
||||
}
|
||||
|
||||
MERGED_PERMISSIONS = {
|
||||
key: {**AUDIT_PERMISSIONS.get(key, {}), **POWER_PERMISSIONS.get(key, {})}
|
||||
for key in set(AUDIT_PERMISSIONS) | set(POWER_PERMISSIONS)
|
||||
key: {
|
||||
**AUDIT_PERMISSIONS.get(key, {}),
|
||||
**POWER_PERMISSIONS.get(key, {}),
|
||||
**SNAPSHOT_PERMISSIONS.get(key, {}),
|
||||
}
|
||||
for key in set(AUDIT_PERMISSIONS)
|
||||
| set(POWER_PERMISSIONS)
|
||||
| set(SNAPSHOT_PERMISSIONS)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user