mirror of
https://github.com/home-assistant/core.git
synced 2026-04-18 23:49:03 +02:00
Compare commits
1 Commits
2026.4.2
...
frenck-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
238859b124 |
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Get information
|
||||
id: info
|
||||
uses: home-assistant/actions/helpers/info@5f5b077d63a1e4c53019231409a0c4d791fb74e5 # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/info@master # zizmor: ignore[unpinned-uses]
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
|
||||
@@ -174,6 +174,7 @@ homeassistant.components.dnsip.*
|
||||
homeassistant.components.doorbird.*
|
||||
homeassistant.components.dormakaba_dkey.*
|
||||
homeassistant.components.downloader.*
|
||||
homeassistant.components.dropbox.*
|
||||
homeassistant.components.droplet.*
|
||||
homeassistant.components.dsmr.*
|
||||
homeassistant.components.duckdns.*
|
||||
@@ -273,12 +274,10 @@ homeassistant.components.homekit_controller.storage
|
||||
homeassistant.components.homekit_controller.utils
|
||||
homeassistant.components.homewizard.*
|
||||
homeassistant.components.homeworks.*
|
||||
homeassistant.components.hr_energy_qube.*
|
||||
homeassistant.components.http.*
|
||||
homeassistant.components.huawei_lte.*
|
||||
homeassistant.components.humidifier.*
|
||||
homeassistant.components.husqvarna_automower.*
|
||||
homeassistant.components.huum.*
|
||||
homeassistant.components.hydrawise.*
|
||||
homeassistant.components.hyperion.*
|
||||
homeassistant.components.hypontech.*
|
||||
@@ -328,7 +327,6 @@ homeassistant.components.ld2410_ble.*
|
||||
homeassistant.components.led_ble.*
|
||||
homeassistant.components.lektrico.*
|
||||
homeassistant.components.letpot.*
|
||||
homeassistant.components.lg_infrared.*
|
||||
homeassistant.components.libre_hardware_monitor.*
|
||||
homeassistant.components.lidarr.*
|
||||
homeassistant.components.lifx.*
|
||||
|
||||
10
CODEOWNERS
generated
10
CODEOWNERS
generated
@@ -401,6 +401,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/dremel_3d_printer/ @tkdrob
|
||||
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/homeassistant/components/dropbox/ @bdr99
|
||||
/tests/components/dropbox/ @bdr99
|
||||
/homeassistant/components/droplet/ @sarahseidman
|
||||
/tests/components/droplet/ @sarahseidman
|
||||
/homeassistant/components/dsmr/ @Robbie1221
|
||||
@@ -737,8 +739,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/homewizard/ @DCSBL
|
||||
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
||||
/tests/components/honeywell/ @rdfurman @mkmer
|
||||
/homeassistant/components/hr_energy_qube/ @MattieGit
|
||||
/tests/components/hr_energy_qube/ @MattieGit
|
||||
/homeassistant/components/html5/ @alexyao2015
|
||||
/tests/components/html5/ @alexyao2015
|
||||
/homeassistant/components/http/ @home-assistant/core
|
||||
@@ -947,8 +947,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/lektrico/ @lektrico
|
||||
/homeassistant/components/letpot/ @jpelgrom
|
||||
/tests/components/letpot/ @jpelgrom
|
||||
/homeassistant/components/lg_infrared/ @home-assistant/core
|
||||
/tests/components/lg_infrared/ @home-assistant/core
|
||||
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||
@@ -1077,8 +1075,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/modern_forms/ @wonderslug
|
||||
/homeassistant/components/moehlenhoff_alpha2/ @j-a-n
|
||||
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
||||
/homeassistant/components/moisture/ @home-assistant/core
|
||||
/tests/components/moisture/ @home-assistant/core
|
||||
/homeassistant/components/monarch_money/ @jeeftor
|
||||
/tests/components/monarch_money/ @jeeftor
|
||||
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
||||
@@ -1762,8 +1758,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tomorrowio/ @raman325 @lymanepp
|
||||
/homeassistant/components/totalconnect/ @austinmroczek
|
||||
/tests/components/totalconnect/ @austinmroczek
|
||||
/homeassistant/components/touchline/ @mnordseth
|
||||
/tests/components/touchline/ @mnordseth
|
||||
/homeassistant/components/touchline_sl/ @jnsgruk
|
||||
/tests/components/touchline_sl/ @jnsgruk
|
||||
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
||||
|
||||
2
Dockerfile
generated
2
Dockerfile
generated
@@ -29,7 +29,7 @@ RUN \
|
||||
# Verify go2rtc can be executed
|
||||
go2rtc --version \
|
||||
# Install uv
|
||||
&& pip3 install uv==0.11.1
|
||||
&& pip3 install uv==0.10.6
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
||||
@@ -238,9 +238,7 @@ DEFAULT_INTEGRATIONS = {
|
||||
"timer",
|
||||
#
|
||||
# Base platforms:
|
||||
# Note: Calendar and todo are not included to prevent them from registering
|
||||
# their frontend panels when there are no calendar or todo integrations.
|
||||
*(BASE_PLATFORMS - {"calendar", "todo"}),
|
||||
*BASE_PLATFORMS,
|
||||
#
|
||||
# Integrations providing triggers and conditions for base platforms:
|
||||
"air_quality",
|
||||
@@ -250,7 +248,6 @@ DEFAULT_INTEGRATIONS = {
|
||||
"gate",
|
||||
"humidity",
|
||||
"illuminance",
|
||||
"moisture",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"power",
|
||||
@@ -470,7 +467,6 @@ 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()),
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "bega",
|
||||
"name": "BEGA",
|
||||
"iot_standards": ["zigbee"]
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"domain": "lg",
|
||||
"name": "LG",
|
||||
"integrations": [
|
||||
"lg_infrared",
|
||||
"lg_netcast",
|
||||
"lg_soundbar",
|
||||
"lg_thinq",
|
||||
"webostv"
|
||||
]
|
||||
"integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
},
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["jaraco.abode", "lomond"],
|
||||
"requirements": ["jaraco.abode==6.4.0"],
|
||||
"requirements": ["jaraco.abode==6.2.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
||||
@@ -41,7 +40,6 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
||||
AbodeSensorDescription(
|
||||
key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
|
||||
device.temp_unit
|
||||
],
|
||||
@@ -50,14 +48,12 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
||||
AbodeSensorDescription(
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
|
||||
value_fn=lambda device: cast(float, device.humidity),
|
||||
),
|
||||
AbodeSensorDescription(
|
||||
key="lux",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
|
||||
value_fn=lambda device: cast(float, device.lux),
|
||||
),
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
"""Provides conditions for air quality."""
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
make_entity_numerical_condition,
|
||||
make_entity_numerical_condition_with_unit,
|
||||
make_entity_state_condition,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import (
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
MassVolumeConcentrationConverter,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
OzoneConcentrationConverter,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
UnitlessRatioConverter,
|
||||
)
|
||||
|
||||
|
||||
def _make_detected_condition(
|
||||
device_class: BinarySensorDeviceClass,
|
||||
) -> type[Condition]:
|
||||
"""Create a detected condition for a binary sensor device class."""
|
||||
return make_entity_state_condition(
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
|
||||
)
|
||||
|
||||
|
||||
def _make_cleared_condition(
|
||||
device_class: BinarySensorDeviceClass,
|
||||
) -> type[Condition]:
|
||||
"""Create a cleared condition for a binary sensor device class."""
|
||||
return make_entity_state_condition(
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
|
||||
)
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
# Binary sensor conditions (detected/cleared)
|
||||
"is_gas_detected": _make_detected_condition(BinarySensorDeviceClass.GAS),
|
||||
"is_gas_cleared": _make_cleared_condition(BinarySensorDeviceClass.GAS),
|
||||
"is_co_detected": _make_detected_condition(BinarySensorDeviceClass.CO),
|
||||
"is_co_cleared": _make_cleared_condition(BinarySensorDeviceClass.CO),
|
||||
"is_smoke_detected": _make_detected_condition(BinarySensorDeviceClass.SMOKE),
|
||||
"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: DomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"is_ozone_value": make_entity_numerical_condition_with_unit(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
OzoneConcentrationConverter,
|
||||
),
|
||||
"is_voc_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
MassVolumeConcentrationConverter,
|
||||
),
|
||||
"is_voc_ratio_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
},
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
UnitlessRatioConverter,
|
||||
),
|
||||
"is_no_value": make_entity_numerical_condition_with_unit(
|
||||
{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: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
),
|
||||
"is_so2_value": make_entity_numerical_condition_with_unit(
|
||||
{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: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"is_pm1_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm25_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm4_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm10_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_n2o_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the air quality conditions."""
|
||||
return CONDITIONS
|
||||
@@ -1,449 +0,0 @@
|
||||
# --- Common condition fields ---
|
||||
|
||||
.condition_behavior: &condition_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
# --- Unit lists for multi-unit pollutants ---
|
||||
|
||||
.co_units: &co_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "mg/m³"
|
||||
- "μg/m³"
|
||||
|
||||
.ozone_units: &ozone_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.voc_units: &voc_units
|
||||
- "μg/m³"
|
||||
- "mg/m³"
|
||||
|
||||
.voc_ratio_units: &voc_ratio_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
|
||||
.no_units: &no_units
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
.no2_units: &no2_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.so2_units: &so2_units
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
# --- Entity filter anchors ---
|
||||
|
||||
.co_threshold_entity: &co_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *co_units
|
||||
- domain: sensor
|
||||
device_class: carbon_monoxide
|
||||
- domain: number
|
||||
device_class: carbon_monoxide
|
||||
|
||||
.co2_threshold_entity: &co2_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "ppm"
|
||||
- domain: sensor
|
||||
device_class: carbon_dioxide
|
||||
- domain: number
|
||||
device_class: carbon_dioxide
|
||||
|
||||
.pm1_threshold_entity: &pm1_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm1
|
||||
- domain: number
|
||||
device_class: pm1
|
||||
|
||||
.pm25_threshold_entity: &pm25_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm25
|
||||
- domain: number
|
||||
device_class: pm25
|
||||
|
||||
.pm4_threshold_entity: &pm4_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm4
|
||||
- domain: number
|
||||
device_class: pm4
|
||||
|
||||
.pm10_threshold_entity: &pm10_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm10
|
||||
- domain: number
|
||||
device_class: pm10
|
||||
|
||||
.ozone_threshold_entity: &ozone_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *ozone_units
|
||||
- domain: sensor
|
||||
device_class: ozone
|
||||
- domain: number
|
||||
device_class: ozone
|
||||
|
||||
.voc_threshold_entity: &voc_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *voc_units
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds
|
||||
|
||||
.voc_ratio_threshold_entity: &voc_ratio_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *voc_ratio_units
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds_parts
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds_parts
|
||||
|
||||
.no_threshold_entity: &no_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *no_units
|
||||
- domain: sensor
|
||||
device_class: nitrogen_monoxide
|
||||
- domain: number
|
||||
device_class: nitrogen_monoxide
|
||||
|
||||
.no2_threshold_entity: &no2_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *no2_units
|
||||
- domain: sensor
|
||||
device_class: nitrogen_dioxide
|
||||
- domain: number
|
||||
device_class: nitrogen_dioxide
|
||||
|
||||
.n2o_threshold_entity: &n2o_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: nitrous_oxide
|
||||
- domain: number
|
||||
device_class: nitrous_oxide
|
||||
|
||||
.so2_threshold_entity: &so2_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *so2_units
|
||||
- domain: sensor
|
||||
device_class: sulphur_dioxide
|
||||
- domain: number
|
||||
device_class: sulphur_dioxide
|
||||
|
||||
# --- Number anchors for single-unit pollutants ---
|
||||
|
||||
.co2_threshold_number: &co2_threshold_number
|
||||
mode: box
|
||||
unit_of_measurement: "ppm"
|
||||
|
||||
.ugm3_threshold_number: &ugm3_threshold_number
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
|
||||
# --- Binary sensor targets ---
|
||||
|
||||
.target_gas: &target_gas
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: gas
|
||||
|
||||
.target_co_binary: &target_co_binary
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: carbon_monoxide
|
||||
|
||||
.target_smoke: &target_smoke
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: smoke
|
||||
|
||||
# --- Sensor targets ---
|
||||
|
||||
.target_co_sensor: &target_co_sensor
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: carbon_monoxide
|
||||
|
||||
.target_co2: &target_co2
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: carbon_dioxide
|
||||
|
||||
.target_pm1: &target_pm1
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: pm1
|
||||
|
||||
.target_pm25: &target_pm25
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: pm25
|
||||
|
||||
.target_pm4: &target_pm4
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: pm4
|
||||
|
||||
.target_pm10: &target_pm10
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: pm10
|
||||
|
||||
.target_ozone: &target_ozone
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: ozone
|
||||
|
||||
.target_voc: &target_voc
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds
|
||||
|
||||
.target_voc_ratio: &target_voc_ratio
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds_parts
|
||||
|
||||
.target_no: &target_no
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: nitrogen_monoxide
|
||||
|
||||
.target_no2: &target_no2
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: nitrogen_dioxide
|
||||
|
||||
.target_n2o: &target_n2o
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: nitrous_oxide
|
||||
|
||||
.target_so2: &target_so2
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: sulphur_dioxide
|
||||
|
||||
# --- Binary sensor conditions ---
|
||||
|
||||
.condition_binary_common: &condition_binary_common
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
|
||||
is_gas_detected:
|
||||
<<: *condition_binary_common
|
||||
target: *target_gas
|
||||
|
||||
is_gas_cleared:
|
||||
<<: *condition_binary_common
|
||||
target: *target_gas
|
||||
|
||||
is_co_detected:
|
||||
<<: *condition_binary_common
|
||||
target: *target_co_binary
|
||||
|
||||
is_co_cleared:
|
||||
<<: *condition_binary_common
|
||||
target: *target_co_binary
|
||||
|
||||
is_smoke_detected:
|
||||
<<: *condition_binary_common
|
||||
target: *target_smoke
|
||||
|
||||
is_smoke_cleared:
|
||||
<<: *condition_binary_common
|
||||
target: *target_smoke
|
||||
|
||||
# --- Numerical sensor conditions with unit conversion ---
|
||||
|
||||
is_co_value:
|
||||
target: *target_co_sensor
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *co_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *co_units
|
||||
|
||||
is_ozone_value:
|
||||
target: *target_ozone
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *ozone_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *ozone_units
|
||||
|
||||
is_voc_value:
|
||||
target: *target_voc
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *voc_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *voc_units
|
||||
|
||||
is_voc_ratio_value:
|
||||
target: *target_voc_ratio
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *voc_ratio_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *voc_ratio_units
|
||||
|
||||
is_no_value:
|
||||
target: *target_no
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *no_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *no_units
|
||||
|
||||
is_no2_value:
|
||||
target: *target_no2
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *no2_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *no2_units
|
||||
|
||||
is_so2_value:
|
||||
target: *target_so2
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *so2_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *so2_units
|
||||
|
||||
# --- Numerical sensor conditions without unit conversion ---
|
||||
|
||||
is_co2_value:
|
||||
target: *target_co2
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *co2_threshold_entity
|
||||
mode: is
|
||||
number: *co2_threshold_number
|
||||
|
||||
is_pm1_value:
|
||||
target: *target_pm1
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm1_threshold_entity
|
||||
mode: is
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
is_pm25_value:
|
||||
target: *target_pm25
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm25_threshold_entity
|
||||
mode: is
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
is_pm4_value:
|
||||
target: *target_pm4
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm4_threshold_entity
|
||||
mode: is
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
is_pm10_value:
|
||||
target: *target_pm10
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm10_threshold_entity
|
||||
mode: is
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
is_n2o_value:
|
||||
target: *target_n2o
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *n2o_threshold_entity
|
||||
mode: is
|
||||
number: *ugm3_threshold_number
|
||||
@@ -1,63 +1,4 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_co2_value": {
|
||||
"condition": "mdi:molecule-co2"
|
||||
},
|
||||
"is_co_cleared": {
|
||||
"condition": "mdi:check-circle"
|
||||
},
|
||||
"is_co_detected": {
|
||||
"condition": "mdi:molecule-co"
|
||||
},
|
||||
"is_co_value": {
|
||||
"condition": "mdi:molecule-co"
|
||||
},
|
||||
"is_gas_cleared": {
|
||||
"condition": "mdi:check-circle"
|
||||
},
|
||||
"is_gas_detected": {
|
||||
"condition": "mdi:gas-cylinder"
|
||||
},
|
||||
"is_n2o_value": {
|
||||
"condition": "mdi:factory"
|
||||
},
|
||||
"is_no2_value": {
|
||||
"condition": "mdi:factory"
|
||||
},
|
||||
"is_no_value": {
|
||||
"condition": "mdi:factory"
|
||||
},
|
||||
"is_ozone_value": {
|
||||
"condition": "mdi:weather-sunny-alert"
|
||||
},
|
||||
"is_pm10_value": {
|
||||
"condition": "mdi:blur"
|
||||
},
|
||||
"is_pm1_value": {
|
||||
"condition": "mdi:blur"
|
||||
},
|
||||
"is_pm25_value": {
|
||||
"condition": "mdi:blur"
|
||||
},
|
||||
"is_pm4_value": {
|
||||
"condition": "mdi:blur"
|
||||
},
|
||||
"is_smoke_cleared": {
|
||||
"condition": "mdi:check-circle"
|
||||
},
|
||||
"is_smoke_detected": {
|
||||
"condition": "mdi:smoke-detector-variant"
|
||||
},
|
||||
"is_so2_value": {
|
||||
"condition": "mdi:factory"
|
||||
},
|
||||
"is_voc_ratio_value": {
|
||||
"condition": "mdi:air-filter"
|
||||
},
|
||||
"is_voc_value": {
|
||||
"condition": "mdi:air-filter"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:air-filter"
|
||||
|
||||
@@ -1,227 +1,23 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_threshold_name": "Threshold type"
|
||||
},
|
||||
"conditions": {
|
||||
"is_co2_value": {
|
||||
"description": "Tests the carbon dioxide level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon dioxide value"
|
||||
},
|
||||
"is_co_cleared": {
|
||||
"description": "Tests if one or more carbon monoxide sensors are cleared.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide cleared"
|
||||
},
|
||||
"is_co_detected": {
|
||||
"description": "Tests if one or more carbon monoxide sensors are detecting carbon monoxide.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide detected"
|
||||
},
|
||||
"is_co_value": {
|
||||
"description": "Tests the carbon monoxide level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide value"
|
||||
},
|
||||
"is_gas_cleared": {
|
||||
"description": "Tests if one or more gas sensors are cleared.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas cleared"
|
||||
},
|
||||
"is_gas_detected": {
|
||||
"description": "Tests if one or more gas sensors are detecting gas.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas detected"
|
||||
},
|
||||
"is_n2o_value": {
|
||||
"description": "Tests the nitrous oxide level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrous oxide value"
|
||||
},
|
||||
"is_no2_value": {
|
||||
"description": "Tests the nitrogen dioxide level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen dioxide value"
|
||||
},
|
||||
"is_no_value": {
|
||||
"description": "Tests the nitrogen monoxide level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen monoxide value"
|
||||
},
|
||||
"is_ozone_value": {
|
||||
"description": "Tests the ozone level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Ozone value"
|
||||
},
|
||||
"is_pm10_value": {
|
||||
"description": "Tests the PM10 level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM10 value"
|
||||
},
|
||||
"is_pm1_value": {
|
||||
"description": "Tests the PM1 level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM1 value"
|
||||
},
|
||||
"is_pm25_value": {
|
||||
"description": "Tests the PM2.5 level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM2.5 value"
|
||||
},
|
||||
"is_pm4_value": {
|
||||
"description": "Tests the PM4 level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM4 value"
|
||||
},
|
||||
"is_smoke_cleared": {
|
||||
"description": "Tests if one or more smoke sensors are cleared.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke cleared"
|
||||
},
|
||||
"is_smoke_detected": {
|
||||
"description": "Tests if one or more smoke sensors are detecting smoke.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke detected"
|
||||
},
|
||||
"is_so2_value": {
|
||||
"description": "Tests the sulphur dioxide level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Sulphur dioxide value"
|
||||
},
|
||||
"is_voc_ratio_value": {
|
||||
"description": "Tests the volatile organic compounds ratio of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds ratio value"
|
||||
},
|
||||
"is_voc_value": {
|
||||
"description": "Tests the volatile organic compounds level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds value"
|
||||
}
|
||||
"trigger_behavior_description": "The behavior of the targeted entities to trigger on.",
|
||||
"trigger_behavior_name": "Behavior",
|
||||
"trigger_changed_above_name": "Above",
|
||||
"trigger_changed_below_name": "Below",
|
||||
"trigger_threshold_lower_limit_description": "The lower limit of the threshold.",
|
||||
"trigger_threshold_lower_limit_name": "Lower limit",
|
||||
"trigger_threshold_type_description": "The type of threshold to use.",
|
||||
"trigger_threshold_type_name": "Threshold type",
|
||||
"trigger_threshold_upper_limit_description": "The upper limit of the threshold.",
|
||||
"trigger_threshold_upper_limit_name": "Upper limit",
|
||||
"trigger_unit_description": "All values will be converted to this unit when evaluating the trigger.",
|
||||
"trigger_unit_name": "Unit of measurement"
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
"number": "Number"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
@@ -230,6 +26,14 @@
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
},
|
||||
"trigger_threshold_type": {
|
||||
"options": {
|
||||
"above": "Above",
|
||||
"below": "Below",
|
||||
"between": "Between",
|
||||
"outside": "Outside"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Air Quality",
|
||||
@@ -237,8 +41,13 @@
|
||||
"co2_changed": {
|
||||
"description": "Triggers after one or more carbon dioxide levels change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when carbon dioxide level is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when carbon dioxide level is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon dioxide level changed"
|
||||
@@ -247,10 +56,20 @@
|
||||
"description": "Triggers after one or more carbon dioxide levels cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon dioxide level crossed threshold"
|
||||
@@ -258,8 +77,17 @@
|
||||
"co_changed": {
|
||||
"description": "Triggers after one or more carbon monoxide levels change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when carbon monoxide level is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when carbon monoxide level is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide level changed"
|
||||
@@ -268,6 +96,7 @@
|
||||
"description": "Triggers after one or more carbon monoxide sensors stop detecting carbon monoxide.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -277,10 +106,24 @@
|
||||
"description": "Triggers after one or more carbon monoxide levels cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide level crossed threshold"
|
||||
@@ -289,6 +132,7 @@
|
||||
"description": "Triggers after one or more carbon monoxide sensors start detecting carbon monoxide.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -298,6 +142,7 @@
|
||||
"description": "Triggers after one or more gas sensors stop detecting gas.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -307,6 +152,7 @@
|
||||
"description": "Triggers after one or more gas sensors start detecting gas.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -315,8 +161,13 @@
|
||||
"n2o_changed": {
|
||||
"description": "Triggers after one or more nitrous oxide levels change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when nitrous oxide level is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when nitrous oxide level is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrous oxide level changed"
|
||||
@@ -325,10 +176,20 @@
|
||||
"description": "Triggers after one or more nitrous oxide levels cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrous oxide level crossed threshold"
|
||||
@@ -336,8 +197,17 @@
|
||||
"no2_changed": {
|
||||
"description": "Triggers after one or more nitrogen dioxide levels change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when nitrogen dioxide level is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when nitrogen dioxide level is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen dioxide level changed"
|
||||
@@ -346,10 +216,24 @@
|
||||
"description": "Triggers after one or more nitrogen dioxide levels cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen dioxide level crossed threshold"
|
||||
@@ -357,8 +241,17 @@
|
||||
"no_changed": {
|
||||
"description": "Triggers after one or more nitrogen monoxide levels change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when nitrogen monoxide level is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when nitrogen monoxide level is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen monoxide level changed"
|
||||
@@ -367,10 +260,24 @@
|
||||
"description": "Triggers after one or more nitrogen monoxide levels cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen monoxide level crossed threshold"
|
||||
@@ -378,8 +285,17 @@
|
||||
"ozone_changed": {
|
||||
"description": "Triggers after one or more ozone levels change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when ozone level is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when ozone level is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Ozone level changed"
|
||||
@@ -388,10 +304,24 @@
|
||||
"description": "Triggers after one or more ozone levels cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Ozone level crossed threshold"
|
||||
@@ -399,8 +329,13 @@
|
||||
"pm10_changed": {
|
||||
"description": "Triggers after one or more PM10 levels change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when PM10 level is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when PM10 level is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM10 level changed"
|
||||
@@ -409,10 +344,20 @@
|
||||
"description": "Triggers after one or more PM10 levels cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM10 level crossed threshold"
|
||||
@@ -420,8 +365,13 @@
|
||||
"pm1_changed": {
|
||||
"description": "Triggers after one or more PM1 levels change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when PM1 level is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when PM1 level is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM1 level changed"
|
||||
@@ -430,10 +380,20 @@
|
||||
"description": "Triggers after one or more PM1 levels cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM1 level crossed threshold"
|
||||
@@ -441,8 +401,13 @@
|
||||
"pm25_changed": {
|
||||
"description": "Triggers after one or more PM2.5 levels change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when PM2.5 level is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when PM2.5 level is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM2.5 level changed"
|
||||
@@ -451,10 +416,20 @@
|
||||
"description": "Triggers after one or more PM2.5 levels cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM2.5 level crossed threshold"
|
||||
@@ -462,8 +437,13 @@
|
||||
"pm4_changed": {
|
||||
"description": "Triggers after one or more PM4 levels change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when PM4 level is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when PM4 level is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM4 level changed"
|
||||
@@ -472,10 +452,20 @@
|
||||
"description": "Triggers after one or more PM4 levels cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM4 level crossed threshold"
|
||||
@@ -484,6 +474,7 @@
|
||||
"description": "Triggers after one or more smoke sensors stop detecting smoke.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -493,6 +484,7 @@
|
||||
"description": "Triggers after one or more smoke sensors start detecting smoke.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -501,8 +493,17 @@
|
||||
"so2_changed": {
|
||||
"description": "Triggers after one or more sulphur dioxide levels change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when sulphur dioxide level is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when sulphur dioxide level is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Sulphur dioxide level changed"
|
||||
@@ -511,10 +512,24 @@
|
||||
"description": "Triggers after one or more sulphur dioxide levels cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Sulphur dioxide level crossed threshold"
|
||||
@@ -522,8 +537,17 @@
|
||||
"voc_changed": {
|
||||
"description": "Triggers after one or more volatile organic compound levels change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when volatile organic compounds level is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when volatile organic compounds level is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds level changed"
|
||||
@@ -532,10 +556,24 @@
|
||||
"description": "Triggers after one or more volatile organic compounds levels cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds level crossed threshold"
|
||||
@@ -543,8 +581,17 @@
|
||||
"voc_ratio_changed": {
|
||||
"description": "Triggers after one or more volatile organic compound ratios change.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Only trigger when volatile organic compounds ratio is above this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_above_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when volatile organic compounds ratio is below this value.",
|
||||
"name": "[%key:component::air_quality::common::trigger_changed_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds ratio changed"
|
||||
@@ -553,10 +600,24 @@
|
||||
"description": "Triggers after one or more volatile organic compounds ratios cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_lower_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_lower_limit_name%]"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_type_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_type_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_unit_name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "[%key:component::air_quality::common::trigger_threshold_upper_limit_description%]",
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_upper_limit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds ratio crossed threshold"
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
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: DomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"co_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"ozone_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
OzoneConcentrationConverter,
|
||||
),
|
||||
"ozone_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
OzoneConcentrationConverter,
|
||||
),
|
||||
"voc_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
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: DomainSpec(
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
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: DomainSpec(
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
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: DomainSpec(
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
},
|
||||
@@ -120,82 +120,114 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
UnitlessRatioConverter,
|
||||
),
|
||||
"no_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
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: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
),
|
||||
"no2_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
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: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
),
|
||||
"so2_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
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: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
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: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"co2_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"pm1_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm1_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm25_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm25_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm4_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm4_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm10_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm10_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"n2o_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROUS_OXIDE
|
||||
)
|
||||
},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"n2o_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROUS_OXIDE
|
||||
)
|
||||
},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -10,155 +10,374 @@
|
||||
- last
|
||||
- any
|
||||
|
||||
# --- Unit lists for multi-unit pollutants ---
|
||||
.number_or_entity_co: &number_or_entity_co
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "mg/m³"
|
||||
- "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: carbon_monoxide
|
||||
- domain: number
|
||||
device_class: carbon_monoxide
|
||||
translation_key: number_or_entity
|
||||
|
||||
.co_units: &co_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "mg/m³"
|
||||
- "μg/m³"
|
||||
.number_or_entity_co2: &number_or_entity_co2
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "ppm"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "ppm"
|
||||
- domain: sensor
|
||||
device_class: carbon_dioxide
|
||||
- domain: number
|
||||
device_class: carbon_dioxide
|
||||
translation_key: number_or_entity
|
||||
|
||||
.ozone_units: &ozone_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
.number_or_entity_pm1: &number_or_entity_pm1
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm1
|
||||
- domain: number
|
||||
device_class: pm1
|
||||
translation_key: number_or_entity
|
||||
|
||||
.voc_units: &voc_units
|
||||
- "μg/m³"
|
||||
- "mg/m³"
|
||||
.number_or_entity_pm25: &number_or_entity_pm25
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm25
|
||||
- domain: number
|
||||
device_class: pm25
|
||||
translation_key: number_or_entity
|
||||
|
||||
.voc_ratio_units: &voc_ratio_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
.number_or_entity_pm4: &number_or_entity_pm4
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm4
|
||||
- domain: number
|
||||
device_class: pm4
|
||||
translation_key: number_or_entity
|
||||
|
||||
.no_units: &no_units
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
.number_or_entity_pm10: &number_or_entity_pm10
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm10
|
||||
- domain: number
|
||||
device_class: pm10
|
||||
translation_key: number_or_entity
|
||||
|
||||
.no2_units: &no2_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
.number_or_entity_ozone: &number_or_entity_ozone
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: ozone
|
||||
- domain: number
|
||||
device_class: ozone
|
||||
translation_key: number_or_entity
|
||||
|
||||
.so2_units: &so2_units
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
.number_or_entity_voc: &number_or_entity_voc
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "μg/m³"
|
||||
- "mg/m³"
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds
|
||||
translation_key: number_or_entity
|
||||
|
||||
# --- Entity filter anchors ---
|
||||
.number_or_entity_voc_ratio: &number_or_entity_voc_ratio
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds_parts
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds_parts
|
||||
translation_key: number_or_entity
|
||||
|
||||
.co_threshold_entity: &co_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *co_units
|
||||
- domain: sensor
|
||||
device_class: carbon_monoxide
|
||||
- domain: number
|
||||
device_class: carbon_monoxide
|
||||
.number_or_entity_no: &number_or_entity_no
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: nitrogen_monoxide
|
||||
- domain: number
|
||||
device_class: nitrogen_monoxide
|
||||
translation_key: number_or_entity
|
||||
|
||||
.co2_threshold_entity: &co2_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "ppm"
|
||||
- domain: sensor
|
||||
device_class: carbon_dioxide
|
||||
- domain: number
|
||||
device_class: carbon_dioxide
|
||||
.number_or_entity_no2: &number_or_entity_no2
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: nitrogen_dioxide
|
||||
- domain: number
|
||||
device_class: nitrogen_dioxide
|
||||
translation_key: number_or_entity
|
||||
|
||||
.pm1_threshold_entity: &pm1_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm1
|
||||
- domain: number
|
||||
device_class: pm1
|
||||
.number_or_entity_n2o: &number_or_entity_n2o
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: nitrous_oxide
|
||||
- domain: number
|
||||
device_class: nitrous_oxide
|
||||
translation_key: number_or_entity
|
||||
|
||||
.pm25_threshold_entity: &pm25_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm25
|
||||
- domain: number
|
||||
device_class: pm25
|
||||
.number_or_entity_so2: &number_or_entity_so2
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: sulphur_dioxide
|
||||
- domain: number
|
||||
device_class: sulphur_dioxide
|
||||
translation_key: number_or_entity
|
||||
|
||||
.pm4_threshold_entity: &pm4_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm4
|
||||
- domain: number
|
||||
device_class: pm4
|
||||
.unit_co: &unit_co
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "mg/m³"
|
||||
- "μg/m³"
|
||||
|
||||
.pm10_threshold_entity: &pm10_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm10
|
||||
- domain: number
|
||||
device_class: pm10
|
||||
.unit_ozone: &unit_ozone
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.ozone_threshold_entity: &ozone_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *ozone_units
|
||||
- domain: sensor
|
||||
device_class: ozone
|
||||
- domain: number
|
||||
device_class: ozone
|
||||
.unit_no2: &unit_no2
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.voc_threshold_entity: &voc_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *voc_units
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds
|
||||
.unit_no: &unit_no
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
.voc_ratio_threshold_entity: &voc_ratio_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *voc_ratio_units
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds_parts
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds_parts
|
||||
.unit_so2: &unit_so2
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
.no_threshold_entity: &no_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *no_units
|
||||
- domain: sensor
|
||||
device_class: nitrogen_monoxide
|
||||
- domain: number
|
||||
device_class: nitrogen_monoxide
|
||||
.unit_voc: &unit_voc
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "μg/m³"
|
||||
- "mg/m³"
|
||||
|
||||
.no2_threshold_entity: &no2_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *no2_units
|
||||
- domain: sensor
|
||||
device_class: nitrogen_dioxide
|
||||
- domain: number
|
||||
device_class: nitrogen_dioxide
|
||||
.unit_voc_ratio: &unit_voc_ratio
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
|
||||
.n2o_threshold_entity: &n2o_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: nitrous_oxide
|
||||
- domain: number
|
||||
device_class: nitrous_oxide
|
||||
|
||||
.so2_threshold_entity: &so2_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *so2_units
|
||||
- domain: sensor
|
||||
device_class: sulphur_dioxide
|
||||
- domain: number
|
||||
device_class: sulphur_dioxide
|
||||
|
||||
# --- Number anchors for single-unit pollutants ---
|
||||
|
||||
.co2_threshold_number: &co2_threshold_number
|
||||
mode: box
|
||||
unit_of_measurement: "ppm"
|
||||
|
||||
.ugm3_threshold_number: &ugm3_threshold_number
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- above
|
||||
- below
|
||||
- between
|
||||
- outside
|
||||
translation_key: trigger_threshold_type
|
||||
|
||||
# Binary sensor detected/cleared trigger fields
|
||||
.trigger_binary_fields: &trigger_binary_fields
|
||||
@@ -276,342 +495,198 @@ smoke_cleared:
|
||||
|
||||
# --- Numerical sensor triggers ---
|
||||
|
||||
# CO (multi-unit)
|
||||
co_changed:
|
||||
target: *target_co_sensor
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *co_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *co_units
|
||||
above: *number_or_entity_co
|
||||
below: *number_or_entity_co
|
||||
unit: *unit_co
|
||||
|
||||
co_crossed_threshold:
|
||||
target: *target_co_sensor
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *co_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *co_units
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_co
|
||||
upper_limit: *number_or_entity_co
|
||||
unit: *unit_co
|
||||
|
||||
# CO2 (single-unit: ppm)
|
||||
co2_changed:
|
||||
target: *target_co2
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *co2_threshold_entity
|
||||
mode: changed
|
||||
number: *co2_threshold_number
|
||||
above: *number_or_entity_co2
|
||||
below: *number_or_entity_co2
|
||||
|
||||
co2_crossed_threshold:
|
||||
target: *target_co2
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *co2_threshold_entity
|
||||
mode: crossed
|
||||
number: *co2_threshold_number
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_co2
|
||||
upper_limit: *number_or_entity_co2
|
||||
|
||||
# PM1 (single-unit: μg/m³)
|
||||
pm1_changed:
|
||||
target: *target_pm1
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm1_threshold_entity
|
||||
mode: changed
|
||||
number: *ugm3_threshold_number
|
||||
above: *number_or_entity_pm1
|
||||
below: *number_or_entity_pm1
|
||||
|
||||
pm1_crossed_threshold:
|
||||
target: *target_pm1
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm1_threshold_entity
|
||||
mode: crossed
|
||||
number: *ugm3_threshold_number
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_pm1
|
||||
upper_limit: *number_or_entity_pm1
|
||||
|
||||
# PM2.5 (single-unit: μg/m³)
|
||||
pm25_changed:
|
||||
target: *target_pm25
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm25_threshold_entity
|
||||
mode: changed
|
||||
number: *ugm3_threshold_number
|
||||
above: *number_or_entity_pm25
|
||||
below: *number_or_entity_pm25
|
||||
|
||||
pm25_crossed_threshold:
|
||||
target: *target_pm25
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm25_threshold_entity
|
||||
mode: crossed
|
||||
number: *ugm3_threshold_number
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_pm25
|
||||
upper_limit: *number_or_entity_pm25
|
||||
|
||||
# PM4 (single-unit: μg/m³)
|
||||
pm4_changed:
|
||||
target: *target_pm4
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm4_threshold_entity
|
||||
mode: changed
|
||||
number: *ugm3_threshold_number
|
||||
above: *number_or_entity_pm4
|
||||
below: *number_or_entity_pm4
|
||||
|
||||
pm4_crossed_threshold:
|
||||
target: *target_pm4
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm4_threshold_entity
|
||||
mode: crossed
|
||||
number: *ugm3_threshold_number
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_pm4
|
||||
upper_limit: *number_or_entity_pm4
|
||||
|
||||
# PM10 (single-unit: μg/m³)
|
||||
pm10_changed:
|
||||
target: *target_pm10
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm10_threshold_entity
|
||||
mode: changed
|
||||
number: *ugm3_threshold_number
|
||||
above: *number_or_entity_pm10
|
||||
below: *number_or_entity_pm10
|
||||
|
||||
pm10_crossed_threshold:
|
||||
target: *target_pm10
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm10_threshold_entity
|
||||
mode: crossed
|
||||
number: *ugm3_threshold_number
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_pm10
|
||||
upper_limit: *number_or_entity_pm10
|
||||
|
||||
# Ozone (multi-unit)
|
||||
ozone_changed:
|
||||
target: *target_ozone
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *ozone_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *ozone_units
|
||||
above: *number_or_entity_ozone
|
||||
below: *number_or_entity_ozone
|
||||
unit: *unit_ozone
|
||||
|
||||
ozone_crossed_threshold:
|
||||
target: *target_ozone
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *ozone_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *ozone_units
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_ozone
|
||||
upper_limit: *number_or_entity_ozone
|
||||
unit: *unit_ozone
|
||||
|
||||
# VOC (multi-unit)
|
||||
voc_changed:
|
||||
target: *target_voc
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *voc_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *voc_units
|
||||
above: *number_or_entity_voc
|
||||
below: *number_or_entity_voc
|
||||
unit: *unit_voc
|
||||
|
||||
voc_crossed_threshold:
|
||||
target: *target_voc
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *voc_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *voc_units
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_voc
|
||||
upper_limit: *number_or_entity_voc
|
||||
unit: *unit_voc
|
||||
|
||||
# VOC ratio (multi-unit)
|
||||
voc_ratio_changed:
|
||||
target: *target_voc_ratio
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *voc_ratio_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *voc_ratio_units
|
||||
above: *number_or_entity_voc_ratio
|
||||
below: *number_or_entity_voc_ratio
|
||||
unit: *unit_voc_ratio
|
||||
|
||||
voc_ratio_crossed_threshold:
|
||||
target: *target_voc_ratio
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *voc_ratio_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *voc_ratio_units
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_voc_ratio
|
||||
upper_limit: *number_or_entity_voc_ratio
|
||||
unit: *unit_voc_ratio
|
||||
|
||||
# NO (multi-unit)
|
||||
no_changed:
|
||||
target: *target_no
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *no_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *no_units
|
||||
above: *number_or_entity_no
|
||||
below: *number_or_entity_no
|
||||
unit: *unit_no
|
||||
|
||||
no_crossed_threshold:
|
||||
target: *target_no
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *no_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *no_units
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_no
|
||||
upper_limit: *number_or_entity_no
|
||||
unit: *unit_no
|
||||
|
||||
# NO2 (multi-unit)
|
||||
no2_changed:
|
||||
target: *target_no2
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *no2_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *no2_units
|
||||
above: *number_or_entity_no2
|
||||
below: *number_or_entity_no2
|
||||
unit: *unit_no2
|
||||
|
||||
no2_crossed_threshold:
|
||||
target: *target_no2
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *no2_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *no2_units
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_no2
|
||||
upper_limit: *number_or_entity_no2
|
||||
unit: *unit_no2
|
||||
|
||||
# N2O (single-unit: μg/m³)
|
||||
n2o_changed:
|
||||
target: *target_n2o
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *n2o_threshold_entity
|
||||
mode: changed
|
||||
number: *ugm3_threshold_number
|
||||
above: *number_or_entity_n2o
|
||||
below: *number_or_entity_n2o
|
||||
|
||||
n2o_crossed_threshold:
|
||||
target: *target_n2o
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *n2o_threshold_entity
|
||||
mode: crossed
|
||||
number: *ugm3_threshold_number
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_n2o
|
||||
upper_limit: *number_or_entity_n2o
|
||||
|
||||
# SO2 (multi-unit)
|
||||
so2_changed:
|
||||
target: *target_so2
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *so2_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *so2_units
|
||||
above: *number_or_entity_so2
|
||||
below: *number_or_entity_so2
|
||||
unit: *unit_so2
|
||||
|
||||
so2_crossed_threshold:
|
||||
target: *target_so2
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *so2_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *so2_units
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_so2
|
||||
upper_limit: *number_or_entity_so2
|
||||
unit: *unit_so2
|
||||
|
||||
@@ -87,7 +87,7 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.debug("Successfully connected to %s", user_input[CONF_IP_ADDRESS])
|
||||
|
||||
device_info = await airq.fetch_device_info()
|
||||
await self.async_set_unique_id(device_info["id"], raise_on_progress=False)
|
||||
await self.async_set_unique_id(device_info["id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
_LOGGER.debug("Creating an entry for %s", device_info["name"])
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"incomplete_discovery": "The discovered air-Q device did not provide a device ID. Ensure the firmware is up to date."
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"trigger_behavior_name": "Trigger when"
|
||||
"condition_behavior_description": "How the state should match on the targeted alarms.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted alarms to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_armed": {
|
||||
"description": "Tests if one or more alarms are armed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -17,6 +20,7 @@
|
||||
"description": "Tests if one or more alarms are armed in away mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -26,6 +30,7 @@
|
||||
"description": "Tests if one or more alarms are armed in home mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -35,6 +40,7 @@
|
||||
"description": "Tests if one or more alarms are armed in night mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -44,6 +50,7 @@
|
||||
"description": "Tests if one or more alarms are armed in vacation mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -53,6 +60,7 @@
|
||||
"description": "Tests if one or more alarms are disarmed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -62,6 +70,7 @@
|
||||
"description": "Tests if one or more alarms are triggered.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -164,7 +173,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Arm alarm away"
|
||||
"name": "Arm away"
|
||||
},
|
||||
"alarm_arm_custom_bypass": {
|
||||
"description": "Arms an alarm while allowing to bypass a custom area.",
|
||||
@@ -174,7 +183,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Arm alarm with custom bypass"
|
||||
"name": "Arm with custom bypass"
|
||||
},
|
||||
"alarm_arm_home": {
|
||||
"description": "Arms an alarm in the home mode.",
|
||||
@@ -184,7 +193,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Arm alarm home"
|
||||
"name": "Arm home"
|
||||
},
|
||||
"alarm_arm_night": {
|
||||
"description": "Arms an alarm in the night mode.",
|
||||
@@ -194,7 +203,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Arm alarm night"
|
||||
"name": "Arm night"
|
||||
},
|
||||
"alarm_arm_vacation": {
|
||||
"description": "Arms an alarm in the vacation mode.",
|
||||
@@ -204,7 +213,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Arm alarm vacation"
|
||||
"name": "Arm vacation"
|
||||
},
|
||||
"alarm_disarm": {
|
||||
"description": "Disarms an alarm.",
|
||||
@@ -214,7 +223,7 @@
|
||||
"name": "Code"
|
||||
}
|
||||
},
|
||||
"name": "Disarm alarm"
|
||||
"name": "Disarm"
|
||||
},
|
||||
"alarm_trigger": {
|
||||
"description": "Triggers an alarm manually.",
|
||||
@@ -224,7 +233,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Trigger alarm"
|
||||
"name": "Trigger"
|
||||
}
|
||||
},
|
||||
"title": "Alarm control panel",
|
||||
@@ -233,6 +242,7 @@
|
||||
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -242,6 +252,7 @@
|
||||
"description": "Triggers after one or more alarms become armed in away mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -251,6 +262,7 @@
|
||||
"description": "Triggers after one or more alarms become armed in home mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -260,6 +272,7 @@
|
||||
"description": "Triggers after one or more alarms become armed in night mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -269,6 +282,7 @@
|
||||
"description": "Triggers after one or more alarms become armed in vacation mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -278,6 +292,7 @@
|
||||
"description": "Triggers after one or more alarms become disarmed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -287,6 +302,7 @@
|
||||
"description": "Triggers after one or more alarms become triggered.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -54,16 +54,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_LOGIN_DATA],
|
||||
)
|
||||
device_registry = dr.async_get(hass)
|
||||
self.previous_devices: set[str] = {
|
||||
identifier
|
||||
for device in device_registry.devices.get_devices_for_config_entry_id(
|
||||
entry.entry_id
|
||||
)
|
||||
if device.entry_type != dr.DeviceEntryType.SERVICE
|
||||
for identifier_domain, identifier in device.identifiers
|
||||
if identifier_domain == DOMAIN
|
||||
}
|
||||
self.previous_devices: set[str] = set()
|
||||
|
||||
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
||||
"""Update device data."""
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==13.3.2"]
|
||||
"requirements": ["aioamazondevices==13.3.0"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
from python_homeassistant_analytics import (
|
||||
Environment,
|
||||
HomeassistantAnalyticsClient,
|
||||
HomeassistantAnalyticsConnectionError,
|
||||
)
|
||||
@@ -39,7 +38,7 @@ async def async_setup_entry(
|
||||
client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass))
|
||||
|
||||
try:
|
||||
integrations = await client.get_integrations(Environment.NEXT)
|
||||
integrations = await client.get_integrations()
|
||||
except HomeassistantAnalyticsConnectionError as ex:
|
||||
raise ConfigEntryNotReady("Could not fetch integration list") from ex
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
_LOGGER.debug("Updating statistics for the first time")
|
||||
usage_sum = 0.0
|
||||
last_stats_time = None
|
||||
allow_update_last_stored_hour = False
|
||||
else:
|
||||
if not meter.readings or len(meter.readings) == 0:
|
||||
_LOGGER.debug("No recent usage statistics found, skipping update")
|
||||
@@ -108,7 +107,6 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
continue
|
||||
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
|
||||
_LOGGER.debug("Getting statistics at %s", start)
|
||||
stats: dict[str, list[Any]] = {}
|
||||
for end in (start + timedelta(seconds=1), None):
|
||||
stats = await get_instance(self.hass).async_add_executor_job(
|
||||
statistics_during_period,
|
||||
@@ -129,28 +127,15 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"Not found, trying to find oldest statistic after %s",
|
||||
start,
|
||||
)
|
||||
assert stats
|
||||
|
||||
if not stats or not stats.get(usage_statistic_id):
|
||||
_LOGGER.debug(
|
||||
"Could not find existing statistics during period lookup for %s, "
|
||||
"falling back to last stored statistic",
|
||||
usage_statistic_id,
|
||||
)
|
||||
allow_update_last_stored_hour = True
|
||||
last_records = last_stat[usage_statistic_id]
|
||||
usage_sum = float(last_records[0].get("sum") or 0.0)
|
||||
last_stats_time = last_records[0]["start"]
|
||||
else:
|
||||
allow_update_last_stored_hour = False
|
||||
records = stats[usage_statistic_id]
|
||||
def _safe_get_sum(records: list[Any]) -> float:
|
||||
if records and "sum" in records[0]:
|
||||
return float(records[0]["sum"])
|
||||
return 0.0
|
||||
|
||||
def _safe_get_sum(records: list[Any]) -> float:
|
||||
if records and "sum" in records[0]:
|
||||
return float(records[0]["sum"])
|
||||
return 0.0
|
||||
|
||||
usage_sum = _safe_get_sum(records)
|
||||
last_stats_time = records[0]["start"]
|
||||
usage_sum = _safe_get_sum(stats.get(usage_statistic_id, []))
|
||||
last_stats_time = stats[usage_statistic_id][0]["start"]
|
||||
|
||||
usage_statistics = []
|
||||
|
||||
@@ -163,13 +148,7 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
)
|
||||
continue
|
||||
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
|
||||
if last_stats_time is not None and (
|
||||
start.timestamp() < last_stats_time
|
||||
or (
|
||||
start.timestamp() == last_stats_time
|
||||
and not allow_update_last_stored_hour
|
||||
)
|
||||
):
|
||||
if last_stats_time is not None and start.timestamp() <= last_stats_time:
|
||||
continue
|
||||
usage_state = max(0, read["consumption"] / 1000)
|
||||
usage_sum = max(0, read["read"])
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyanglianwater"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyanglianwater==3.1.2"]
|
||||
"requirements": ["pyanglianwater==3.1.1"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["arcam"],
|
||||
"requirements": ["arcam-fmj==1.8.3"],
|
||||
"requirements": ["arcam-fmj==1.8.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -91,7 +91,6 @@ SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
|
||||
value_fn=lambda state: (
|
||||
vp.colorspace.name.lower()
|
||||
if (vp := state.get_incoming_video_parameters()) is not None
|
||||
and vp.colorspace is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
|
||||
@@ -137,4 +137,5 @@ async def async_pipeline_from_audio_stream(
|
||||
audio_settings=audio_settings or AudioSettings(),
|
||||
),
|
||||
)
|
||||
await pipeline_input.execute(validate=True)
|
||||
await pipeline_input.validate()
|
||||
await pipeline_input.execute()
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
"""Assist pipeline errors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .pipeline import PipelineStage
|
||||
|
||||
|
||||
class PipelineError(HomeAssistantError):
|
||||
"""Base class for pipeline errors."""
|
||||
@@ -62,25 +55,3 @@ class IntentRecognitionError(PipelineError):
|
||||
|
||||
class TextToSpeechError(PipelineError):
|
||||
"""Error in text-to-speech portion of pipeline."""
|
||||
|
||||
|
||||
class PipelineRunValidationError(PipelineError):
|
||||
"""Error when a pipeline run is not valid."""
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
"""Set error message."""
|
||||
super().__init__("validation-error", message)
|
||||
|
||||
|
||||
class InvalidPipelineStagesError(PipelineRunValidationError):
|
||||
"""Error when given an invalid combination of start/end stages."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start_stage: PipelineStage,
|
||||
end_stage: PipelineStage,
|
||||
) -> None:
|
||||
"""Set error message."""
|
||||
super().__init__(
|
||||
f"Invalid stage combination: start={start_stage}, end={end_stage}"
|
||||
)
|
||||
|
||||
@@ -73,10 +73,8 @@ from .const import (
|
||||
from .error import (
|
||||
DuplicateWakeUpDetectedError,
|
||||
IntentRecognitionError,
|
||||
InvalidPipelineStagesError,
|
||||
PipelineError,
|
||||
PipelineNotFound,
|
||||
PipelineRunValidationError,
|
||||
SpeechToTextError,
|
||||
TextToSpeechError,
|
||||
WakeWordDetectionAborted,
|
||||
@@ -494,6 +492,24 @@ PIPELINE_STAGE_ORDER = [
|
||||
]
|
||||
|
||||
|
||||
class PipelineRunValidationError(Exception):
|
||||
"""Error when a pipeline run is not valid."""
|
||||
|
||||
|
||||
class InvalidPipelineStagesError(PipelineRunValidationError):
|
||||
"""Error when given an invalid combination of start/end stages."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start_stage: PipelineStage,
|
||||
end_stage: PipelineStage,
|
||||
) -> None:
|
||||
"""Set error message."""
|
||||
super().__init__(
|
||||
f"Invalid stage combination: start={start_stage}, end={end_stage}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WakeWordSettings:
|
||||
"""Settings for wake word detection."""
|
||||
@@ -1664,39 +1680,26 @@ class PipelineInput:
|
||||
satellite_id: str | None = None
|
||||
"""Identifier of the satellite that is processing the input/output of the pipeline."""
|
||||
|
||||
async def execute(self, validate: bool = False) -> None:
|
||||
async def execute(self) -> None:
|
||||
"""Run pipeline."""
|
||||
validation_error: PipelineError | None = None
|
||||
if validate:
|
||||
try:
|
||||
await self.validate()
|
||||
except PipelineError as err:
|
||||
validation_error = err
|
||||
|
||||
self.run.start(
|
||||
conversation_id=self.session.conversation_id,
|
||||
device_id=self.device_id,
|
||||
satellite_id=self.satellite_id,
|
||||
)
|
||||
current_stage: PipelineStage | None = self.run.start_stage
|
||||
stt_audio_buffer: list[EnhancedAudioChunk] = []
|
||||
stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None
|
||||
|
||||
if self.stt_stream is not None:
|
||||
if self.run.audio_settings.needs_processor:
|
||||
# VAD/noise suppression/auto gain/volume
|
||||
stt_processed_stream = self.run.process_enhance_audio(self.stt_stream)
|
||||
else:
|
||||
# Volume multiplier only
|
||||
stt_processed_stream = self.run.process_volume_only(self.stt_stream)
|
||||
|
||||
try:
|
||||
if validation_error is not None:
|
||||
raise validation_error
|
||||
|
||||
stt_audio_buffer: list[EnhancedAudioChunk] = []
|
||||
stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None
|
||||
|
||||
if self.stt_stream is not None:
|
||||
if self.run.audio_settings.needs_processor:
|
||||
# VAD/noise suppression/auto gain/volume
|
||||
stt_processed_stream = self.run.process_enhance_audio(
|
||||
self.stt_stream
|
||||
)
|
||||
else:
|
||||
# Volume multiplier only
|
||||
stt_processed_stream = self.run.process_volume_only(self.stt_stream)
|
||||
|
||||
if current_stage == PipelineStage.WAKE_WORD:
|
||||
# wake-word-detection
|
||||
assert stt_processed_stream is not None
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"trigger_behavior_name": "Trigger when"
|
||||
"condition_behavior_description": "How the state should match on the targeted Assist satellites.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted Assist satellites to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_idle": {
|
||||
"description": "Tests if one or more Assist satellites are idle.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -17,6 +20,7 @@
|
||||
"description": "Tests if one or more Assist satellites are listening.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -26,6 +30,7 @@
|
||||
"description": "Tests if one or more Assist satellites are processing.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -35,6 +40,7 @@
|
||||
"description": "Tests if one or more Assist satellites are responding.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -75,7 +81,7 @@
|
||||
},
|
||||
"services": {
|
||||
"announce": {
|
||||
"description": "Lets an Assist satellite announce a message.",
|
||||
"description": "Lets a satellite announce a message.",
|
||||
"fields": {
|
||||
"media_id": {
|
||||
"description": "The media ID to announce instead of using text-to-speech.",
|
||||
@@ -94,10 +100,10 @@
|
||||
"name": "Preannounce media ID"
|
||||
}
|
||||
},
|
||||
"name": "Announce on satellite"
|
||||
"name": "Announce"
|
||||
},
|
||||
"ask_question": {
|
||||
"description": "Lets an Assist satellite ask a question and get the user's response.",
|
||||
"description": "Asks a question and gets the user's response.",
|
||||
"fields": {
|
||||
"answers": {
|
||||
"description": "Possible answers to the question.",
|
||||
@@ -124,10 +130,10 @@
|
||||
"name": "Question media ID"
|
||||
}
|
||||
},
|
||||
"name": "Ask question on satellite"
|
||||
"name": "Ask question"
|
||||
},
|
||||
"start_conversation": {
|
||||
"description": "Starts a conversation from an Assist satellite.",
|
||||
"description": "Starts a conversation from a satellite.",
|
||||
"fields": {
|
||||
"extra_system_prompt": {
|
||||
"description": "Provide background information to the AI about the request.",
|
||||
@@ -150,42 +156,46 @@
|
||||
"name": "Message"
|
||||
}
|
||||
},
|
||||
"name": "Start conversation on satellite"
|
||||
"name": "Start conversation"
|
||||
}
|
||||
},
|
||||
"title": "Assist satellite",
|
||||
"triggers": {
|
||||
"idle": {
|
||||
"description": "Triggers after one or more Assist satellites become idle after having processed a command.",
|
||||
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite became idle"
|
||||
},
|
||||
"listening": {
|
||||
"description": "Triggers after one or more Assist satellites start listening for a command from someone.",
|
||||
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite started listening"
|
||||
},
|
||||
"processing": {
|
||||
"description": "Triggers after one or more Assist satellites start processing a command after having heard it.",
|
||||
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite started processing"
|
||||
},
|
||||
"responding": {
|
||||
"description": "Triggers after one or more Assist satellites start responding to a command after having processed it, or start announcing something.",
|
||||
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -626,7 +626,7 @@ def websocket_delete_all_refresh_tokens(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Handle delete all refresh tokens request."""
|
||||
current_refresh_token: RefreshToken | None = None
|
||||
current_refresh_token: RefreshToken
|
||||
remove_failed = False
|
||||
token_type = msg.get("token_type")
|
||||
delete_current_token = msg.get("delete_current_token")
|
||||
@@ -654,7 +654,7 @@ def websocket_delete_all_refresh_tokens(
|
||||
else:
|
||||
connection.send_result(msg["id"], {})
|
||||
|
||||
async def _delete_current_token_soon(current_refresh_token: RefreshToken) -> None:
|
||||
async def _delete_current_token_soon() -> None:
|
||||
"""Delete the current token after a delay.
|
||||
|
||||
We do not want to delete the current token immediately as it will
|
||||
@@ -675,15 +675,13 @@ def websocket_delete_all_refresh_tokens(
|
||||
# the token right away.
|
||||
hass.auth.async_remove_refresh_token(current_refresh_token)
|
||||
|
||||
if (
|
||||
delete_current_token
|
||||
and current_refresh_token
|
||||
and (not limit_token_types or current_refresh_token.token_type == token_type)
|
||||
if delete_current_token and (
|
||||
not limit_token_types or current_refresh_token.token_type == token_type
|
||||
):
|
||||
# Deleting the token will close the connection so we need
|
||||
# to do it with a delay in a tracked task to ensure it still
|
||||
# happens if Home Assistant is shutting down.
|
||||
hass.async_create_task(_delete_current_token_soon(current_refresh_token))
|
||||
hass.async_create_task(_delete_current_token_soon())
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
|
||||
@@ -118,13 +118,10 @@ SERVICE_TRIGGER = "trigger"
|
||||
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
|
||||
|
||||
_EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"air_quality",
|
||||
"alarm_control_panel",
|
||||
"assist_satellite",
|
||||
"battery",
|
||||
"calendar",
|
||||
"climate",
|
||||
"counter",
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"door",
|
||||
@@ -132,27 +129,18 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"garage_door",
|
||||
"gate",
|
||||
"humidifier",
|
||||
"humidity",
|
||||
"illuminance",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"moisture",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"person",
|
||||
"power",
|
||||
"schedule",
|
||||
"select",
|
||||
"siren",
|
||||
"switch",
|
||||
"temperature",
|
||||
"text",
|
||||
"timer",
|
||||
"vacuum",
|
||||
"valve",
|
||||
"water_heater",
|
||||
"window",
|
||||
}
|
||||
|
||||
@@ -160,10 +148,8 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"air_quality",
|
||||
"alarm_control_panel",
|
||||
"assist_satellite",
|
||||
"battery",
|
||||
"button",
|
||||
"climate",
|
||||
"counter",
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"door",
|
||||
@@ -178,7 +164,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"moisture",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"person",
|
||||
@@ -191,10 +176,8 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"switch",
|
||||
"temperature",
|
||||
"text",
|
||||
"todo",
|
||||
"update",
|
||||
"vacuum",
|
||||
"valve",
|
||||
"water_heater",
|
||||
"window",
|
||||
}
|
||||
|
||||
@@ -78,11 +78,11 @@
|
||||
"services": {
|
||||
"reload": {
|
||||
"description": "Reloads the automation configuration.",
|
||||
"name": "Reload automations"
|
||||
"name": "[%key:common::action::reload%]"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Toggles (enable / disable) an automation.",
|
||||
"name": "Toggle automation"
|
||||
"name": "[%key:common::action::toggle%]"
|
||||
},
|
||||
"trigger": {
|
||||
"description": "Triggers the actions of an automation.",
|
||||
@@ -92,7 +92,7 @@
|
||||
"name": "Skip conditions"
|
||||
}
|
||||
},
|
||||
"name": "Trigger automation"
|
||||
"name": "Trigger"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Disables an automation.",
|
||||
@@ -102,11 +102,11 @@
|
||||
"name": "Stop actions"
|
||||
}
|
||||
},
|
||||
"name": "Turn off automation"
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Enables an automation.",
|
||||
"name": "Turn on automation"
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
"title": "Automation"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==68"],
|
||||
"requirements": ["axis==67"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -74,12 +74,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackblazeConfigEntry) ->
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_bucket_name",
|
||||
) from err
|
||||
except exception.BadRequest as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="bad_request",
|
||||
translation_placeholders={"error_message": str(err)},
|
||||
) from err
|
||||
except (
|
||||
exception.B2ConnectionError,
|
||||
exception.B2RequestTimeout,
|
||||
|
||||
@@ -101,7 +101,8 @@ def handle_b2_errors[T](
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except B2Error as err:
|
||||
raise BackupAgentError(f"Failed during {func.__name__}: {err}") from err
|
||||
error_msg = f"Failed during {func.__name__}"
|
||||
raise BackupAgentError(error_msg) from err
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -169,7 +170,8 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
async def _cleanup_failed_upload(self, filename: str) -> None:
|
||||
"""Clean up a partially uploaded file after upload failure."""
|
||||
_LOGGER.warning(
|
||||
"Attempting to delete partially uploaded backup file %s",
|
||||
"Attempting to delete partially uploaded main backup file %s "
|
||||
"due to metadata upload failure",
|
||||
filename,
|
||||
)
|
||||
try:
|
||||
@@ -178,10 +180,11 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
)
|
||||
await self._hass.async_add_executor_job(uploaded_main_file_info.delete)
|
||||
except B2Error:
|
||||
_LOGGER.warning(
|
||||
"Failed to clean up partially uploaded backup file %s;"
|
||||
" manual deletion from Backblaze B2 may be required",
|
||||
_LOGGER.debug(
|
||||
"Failed to clean up partially uploaded main backup file %s. "
|
||||
"Manual intervention may be required to delete it from Backblaze B2",
|
||||
filename,
|
||||
exc_info=True,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
@@ -253,10 +256,9 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
prefixed_metadata_filename,
|
||||
)
|
||||
|
||||
tar_uploaded = False
|
||||
upload_successful = False
|
||||
try:
|
||||
await self._upload_backup_file(prefixed_tar_filename, open_stream, {})
|
||||
tar_uploaded = True
|
||||
_LOGGER.debug(
|
||||
"Main backup file upload finished for %s", prefixed_tar_filename
|
||||
)
|
||||
@@ -268,14 +270,15 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
_LOGGER.debug(
|
||||
"Metadata file upload finished for %s", prefixed_metadata_filename
|
||||
)
|
||||
_LOGGER.debug("Backup upload complete: %s", prefixed_tar_filename)
|
||||
self._invalidate_caches(
|
||||
backup.backup_id, prefixed_tar_filename, prefixed_metadata_filename
|
||||
)
|
||||
except B2Error:
|
||||
if tar_uploaded:
|
||||
upload_successful = True
|
||||
finally:
|
||||
if upload_successful:
|
||||
_LOGGER.debug("Backup upload complete: %s", prefixed_tar_filename)
|
||||
self._invalidate_caches(
|
||||
backup.backup_id, prefixed_tar_filename, prefixed_metadata_filename
|
||||
)
|
||||
else:
|
||||
await self._cleanup_failed_upload(prefixed_tar_filename)
|
||||
raise
|
||||
|
||||
def _upload_metadata_file_sync(
|
||||
self, metadata_content: bytes, filename: str
|
||||
|
||||
@@ -174,14 +174,6 @@ class BackblazeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"Backblaze B2 bucket '%s' does not exist", user_input[CONF_BUCKET]
|
||||
)
|
||||
errors[CONF_BUCKET] = "invalid_bucket_name"
|
||||
except exception.BadRequest as err:
|
||||
_LOGGER.error(
|
||||
"Backblaze B2 API rejected the request for Key ID '%s': %s",
|
||||
user_input[CONF_KEY_ID],
|
||||
err,
|
||||
)
|
||||
errors["base"] = "bad_request"
|
||||
placeholders["error_message"] = str(err)
|
||||
except (
|
||||
exception.B2ConnectionError,
|
||||
exception.B2RequestTimeout,
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["b2sdk"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["b2sdk==2.10.4"]
|
||||
"requirements": ["b2sdk==2.10.1"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"bad_request": "The Backblaze B2 API rejected the request: {error_message}",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_bucket_name": "[%key:component::backblaze_b2::exceptions::invalid_bucket_name::message%]",
|
||||
"invalid_capability": "[%key:component::backblaze_b2::exceptions::invalid_capability::message%]",
|
||||
@@ -61,9 +60,6 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"bad_request": {
|
||||
"message": "The Backblaze B2 API rejected the request: {error_message}"
|
||||
},
|
||||
"cannot_connect": {
|
||||
"message": "Cannot connect to endpoint"
|
||||
},
|
||||
|
||||
@@ -34,4 +34,4 @@ EXCLUDE_DATABASE_FROM_BACKUP = [
|
||||
"home-assistant_v2.db-wal",
|
||||
]
|
||||
|
||||
SECURETAR_CREATE_VERSION = 3
|
||||
SECURETAR_CREATE_VERSION = 2
|
||||
|
||||
@@ -12,7 +12,7 @@ import hashlib
|
||||
import io
|
||||
from itertools import chain
|
||||
import json
|
||||
from pathlib import Path, PurePath, PureWindowsPath
|
||||
from pathlib import Path, PurePath
|
||||
import shutil
|
||||
import sys
|
||||
import tarfile
|
||||
@@ -1957,10 +1957,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
suggested_filename: str,
|
||||
) -> WrittenBackup:
|
||||
"""Receive a backup."""
|
||||
safe_filename = PureWindowsPath(suggested_filename).name
|
||||
if not safe_filename or safe_filename == "..":
|
||||
safe_filename = "backup.tar"
|
||||
temp_file = Path(self.temp_backup_dir, safe_filename)
|
||||
temp_file = Path(self.temp_backup_dir, suggested_filename)
|
||||
|
||||
async_add_executor_job = self._hass.async_add_executor_job
|
||||
await async_add_executor_job(make_backup_dir, self.temp_backup_dir)
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["cronsim==2.7", "securetar==2026.4.0"],
|
||||
"requirements": ["cronsim==2.7", "securetar==2026.2.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ from securetar import (
|
||||
SecureTarFile,
|
||||
SecureTarReadError,
|
||||
SecureTarRootKeyContext,
|
||||
get_archive_max_ciphertext_size,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -432,9 +431,7 @@ class _CipherBackupStreamer:
|
||||
|
||||
def size(self) -> int:
|
||||
"""Return the maximum size of the decrypted or encrypted backup."""
|
||||
return get_archive_max_ciphertext_size( # type: ignore[no-any-return]
|
||||
self._backup.size, SECURETAR_CREATE_VERSION, self._num_tar_files()
|
||||
)
|
||||
return self._backup.size + self._num_tar_files() * tarfile.RECORDSIZE
|
||||
|
||||
def _num_tar_files(self) -> int:
|
||||
"""Return the number of inner tar files."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Integration for battery triggers and conditions."""
|
||||
"""Integration for battery conditions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -26,6 +27,7 @@ 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]] = {
|
||||
|
||||
@@ -14,19 +14,24 @@
|
||||
- all
|
||||
- any
|
||||
|
||||
.battery_threshold_entity: &battery_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
device_class: battery
|
||||
- domain: number
|
||||
device_class: battery
|
||||
|
||||
.battery_threshold_number: &battery_threshold_number
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
unit_of_measurement: "%"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
domain:
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
translation_key: number_or_entity
|
||||
|
||||
is_low: *condition_common
|
||||
|
||||
@@ -53,12 +58,9 @@ is_level:
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: battery
|
||||
- domain: number
|
||||
device_class: battery
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *battery_threshold_entity
|
||||
mode: is
|
||||
number: *battery_threshold_number
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
|
||||
@@ -15,25 +15,5 @@
|
||||
"is_not_low": {
|
||||
"condition": "mdi:battery"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"level_changed": {
|
||||
"trigger": "mdi:battery-unknown"
|
||||
},
|
||||
"level_crossed_threshold": {
|
||||
"trigger": "mdi:battery-alert"
|
||||
},
|
||||
"low": {
|
||||
"trigger": "mdi:battery-alert"
|
||||
},
|
||||
"not_low": {
|
||||
"trigger": "mdi:battery"
|
||||
},
|
||||
"started_charging": {
|
||||
"trigger": "mdi:battery-charging"
|
||||
},
|
||||
"stopped_charging": {
|
||||
"trigger": "mdi:battery"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_threshold_name": "Threshold type"
|
||||
"condition_behavior_description": "How the state should match on the targeted batteries.",
|
||||
"condition_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_charging": {
|
||||
"description": "Tests if one or more batteries are charging.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -18,11 +17,17 @@
|
||||
"is_level": {
|
||||
"description": "Tests the battery level of one or more batteries.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Require the battery percentage to be above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::battery::common::condition_threshold_name%]"
|
||||
"below": {
|
||||
"description": "Require the battery percentage to be below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "Battery level"
|
||||
@@ -31,6 +36,7 @@
|
||||
"description": "Tests if one or more batteries are low.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -40,6 +46,7 @@
|
||||
"description": "Tests if one or more batteries are not charging.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -49,6 +56,7 @@
|
||||
"description": "Tests if one or more batteries are not low.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -62,72 +70,12 @@
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
"number": "Number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Battery",
|
||||
"triggers": {
|
||||
"level_changed": {
|
||||
"description": "Triggers after the battery level of one or more batteries changes.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::battery::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery level changed"
|
||||
},
|
||||
"level_crossed_threshold": {
|
||||
"description": "Triggers after the battery level of one or more batteries crosses a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::battery::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery level crossed threshold"
|
||||
},
|
||||
"low": {
|
||||
"description": "Triggers after one or more batteries become low.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery low"
|
||||
},
|
||||
"not_low": {
|
||||
"description": "Triggers after one or more batteries are no longer low.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery not low"
|
||||
},
|
||||
"started_charging": {
|
||||
"description": "Triggers after one or more batteries start charging.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery started charging"
|
||||
},
|
||||
"stopped_charging": {
|
||||
"description": "Triggers after one or more batteries stop charging.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery stopped charging"
|
||||
}
|
||||
}
|
||||
"title": "Battery"
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
"""Provides triggers for batteries."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
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
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
make_entity_numerical_state_changed_trigger,
|
||||
make_entity_numerical_state_crossed_threshold_trigger,
|
||||
make_entity_target_state_trigger,
|
||||
)
|
||||
|
||||
BATTERY_LOW_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.BATTERY),
|
||||
}
|
||||
|
||||
BATTERY_CHARGING_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
BINARY_SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING
|
||||
),
|
||||
}
|
||||
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
|
||||
}
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"low": make_entity_target_state_trigger(BATTERY_LOW_DOMAIN_SPECS, STATE_ON),
|
||||
"not_low": make_entity_target_state_trigger(BATTERY_LOW_DOMAIN_SPECS, STATE_OFF),
|
||||
"started_charging": make_entity_target_state_trigger(
|
||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON
|
||||
),
|
||||
"stopped_charging": make_entity_target_state_trigger(
|
||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
|
||||
),
|
||||
"level_changed": make_entity_numerical_state_changed_trigger(
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS, valid_unit="%"
|
||||
),
|
||||
"level_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS, valid_unit="%"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for batteries."""
|
||||
return TRIGGERS
|
||||
@@ -1,83 +0,0 @@
|
||||
.trigger_common_fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.battery_threshold_entity: &battery_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: number
|
||||
device_class: battery
|
||||
- domain: sensor
|
||||
device_class: battery
|
||||
|
||||
.battery_threshold_number: &battery_threshold_number
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
|
||||
.trigger_target_battery: &trigger_target_battery
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: battery
|
||||
|
||||
.trigger_target_charging: &trigger_target_charging
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: battery_charging
|
||||
|
||||
.trigger_target_percentage: &trigger_target_percentage
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: battery
|
||||
|
||||
low:
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
target: *trigger_target_battery
|
||||
|
||||
not_low:
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
target: *trigger_target_battery
|
||||
|
||||
started_charging:
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
target: *trigger_target_charging
|
||||
|
||||
stopped_charging:
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
target: *trigger_target_charging
|
||||
|
||||
level_changed:
|
||||
target: *trigger_target_percentage
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *battery_threshold_entity
|
||||
mode: changed
|
||||
number: *battery_threshold_number
|
||||
|
||||
level_crossed_threshold:
|
||||
target: *trigger_target_percentage
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *battery_threshold_entity
|
||||
mode: crossed
|
||||
number: *battery_threshold_number
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyblu==2.0.6"],
|
||||
"requirements": ["pyblu==2.0.5"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_musc._tcp.local."
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
"""The BMW Connected Drive integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
DOMAIN = "bmw_connected_drive"
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up BMW Connected Drive from a config entry."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="integration_removed",
|
||||
translation_placeholders={
|
||||
"entries": "/config/integrations/integration/bmw_connected_drive",
|
||||
"custom_component_url": "https://github.com/kvanbiesen/bmw-cardata-ha",
|
||||
},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
@@ -1,9 +0,0 @@
|
||||
"""The BMW Connected Drive integration config flow."""
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
class BMWConnectedDriveConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for BMW Connected Drive."""
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "BMW Connected Drive",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": []
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"issues": {
|
||||
"integration_removed": {
|
||||
"description": "The BMW Connected Drive integration has been removed from Home Assistant.\n\nIn September 2025, BMW blocked third-party access to their servers by adding additional security measures. For EU-registered cars, a community-developed [custom component]({custom_component_url}) using BMW's CarData API is available as an alternative.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing BMW Connected Drive integration entries]({entries}).",
|
||||
"title": "The BMW Connected Drive integration has been removed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ from bsblan import (
|
||||
BSBLAN,
|
||||
BSBLANAuthError,
|
||||
BSBLANConnectionError,
|
||||
BSBLANError,
|
||||
HotWaterConfig,
|
||||
HotWaterSchedule,
|
||||
HotWaterState,
|
||||
@@ -51,7 +50,7 @@ class BSBLanFastData:
|
||||
|
||||
state: State
|
||||
sensor: Sensor
|
||||
dhw: HotWaterState | None = None
|
||||
dhw: HotWaterState
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -112,6 +111,7 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
|
||||
# This reduces response time significantly (~0.2s per parameter)
|
||||
state = await self.client.state(include=STATE_INCLUDE)
|
||||
sensor = await self.client.sensor(include=SENSOR_INCLUDE)
|
||||
dhw = await self.client.hot_water_state(include=DHW_STATE_INCLUDE)
|
||||
|
||||
except BSBLANAuthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
@@ -126,19 +126,6 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
|
||||
translation_placeholders={"host": host},
|
||||
) from err
|
||||
|
||||
# Fetch DHW state separately - device may not support hot water
|
||||
dhw: HotWaterState | None = None
|
||||
try:
|
||||
dhw = await self.client.hot_water_state(include=DHW_STATE_INCLUDE)
|
||||
except BSBLANError:
|
||||
# Preserve last known DHW state if available (entity may depend on it)
|
||||
if self.data:
|
||||
dhw = self.data.dhw
|
||||
LOGGER.debug(
|
||||
"DHW (Domestic Hot Water) state not available on device at %s",
|
||||
self.config_entry.data[CONF_HOST],
|
||||
)
|
||||
|
||||
return BSBLanFastData(
|
||||
state=state,
|
||||
sensor=sensor,
|
||||
@@ -172,6 +159,13 @@ class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
|
||||
dhw_config = await self.client.hot_water_config(include=DHW_CONFIG_INCLUDE)
|
||||
dhw_schedule = await self.client.hot_water_schedule()
|
||||
|
||||
except AttributeError:
|
||||
# Device does not support DHW functionality
|
||||
LOGGER.debug(
|
||||
"DHW (Domestic Hot Water) not available on device at %s",
|
||||
self.config_entry.data[CONF_HOST],
|
||||
)
|
||||
return BSBLanSlowData()
|
||||
except (BSBLANConnectionError, BSBLANAuthError) as err:
|
||||
# If config update fails, keep existing data
|
||||
LOGGER.debug(
|
||||
@@ -183,13 +177,6 @@ class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
|
||||
return self.data
|
||||
# First fetch failed, return empty data
|
||||
return BSBLanSlowData()
|
||||
except BSBLANError, AttributeError:
|
||||
# Device does not support DHW functionality
|
||||
LOGGER.debug(
|
||||
"DHW (Domestic Hot Water) not available on device at %s",
|
||||
self.config_entry.data[CONF_HOST],
|
||||
)
|
||||
return BSBLanSlowData()
|
||||
|
||||
return BSBLanSlowData(
|
||||
dhw_config=dhw_config,
|
||||
|
||||
@@ -22,9 +22,7 @@ async def async_get_config_entry_diagnostics(
|
||||
"fast_coordinator_data": {
|
||||
"state": data.fast_coordinator.data.state.model_dump(),
|
||||
"sensor": data.fast_coordinator.data.sensor.model_dump(),
|
||||
"dhw": data.fast_coordinator.data.dhw.model_dump()
|
||||
if data.fast_coordinator.data.dhw
|
||||
else None,
|
||||
"dhw": data.fast_coordinator.data.dhw.model_dump(),
|
||||
},
|
||||
"static": data.static.model_dump() if data.static is not None else None,
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
DeviceInfo,
|
||||
@@ -13,7 +10,7 @@ from homeassistant.helpers.device_registry import (
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import BSBLanData
|
||||
from .const import DEFAULT_PORT, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .coordinator import BSBLanCoordinator, BSBLanFastCoordinator, BSBLanSlowCoordinator
|
||||
|
||||
|
||||
@@ -25,8 +22,7 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
|
||||
def __init__(self, coordinator: _T, data: BSBLanData) -> None:
|
||||
"""Initialize BSBLan entity with device info."""
|
||||
super().__init__(coordinator)
|
||||
host = coordinator.config_entry.data[CONF_HOST]
|
||||
port = coordinator.config_entry.data.get(CONF_PORT, DEFAULT_PORT)
|
||||
host = coordinator.config_entry.data["host"]
|
||||
mac = data.device.MAC
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, mac)},
|
||||
@@ -48,7 +44,7 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
|
||||
else None
|
||||
),
|
||||
sw_version=data.device.version,
|
||||
configuration_url=str(URL.build(scheme="http", host=host, port=port)),
|
||||
configuration_url=f"http://{host}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-bsblan==5.1.4"],
|
||||
"requirements": ["python-bsblan==5.1.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from bsblan import BSBLANError, HotWaterState, SetHotWaterParam
|
||||
from bsblan import BSBLANError, SetHotWaterParam
|
||||
|
||||
from homeassistant.components.water_heater import (
|
||||
STATE_ECO,
|
||||
@@ -46,10 +46,8 @@ async def async_setup_entry(
|
||||
data = entry.runtime_data
|
||||
|
||||
# Only create water heater entity if DHW (Domestic Hot Water) is available
|
||||
# Check if we have any DHW-related data indicating water heater support
|
||||
dhw_data = data.fast_coordinator.data.dhw
|
||||
if dhw_data is None:
|
||||
# Device does not support DHW, skip water heater setup
|
||||
return
|
||||
if (
|
||||
dhw_data.operating_mode is None
|
||||
and dhw_data.nominal_setpoint is None
|
||||
@@ -109,21 +107,11 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
|
||||
else:
|
||||
self._attr_max_temp = 65.0 # Default maximum
|
||||
|
||||
@property
|
||||
def _dhw(self) -> HotWaterState:
|
||||
"""Return DHW state data.
|
||||
|
||||
This entity is only created when DHW data is available.
|
||||
"""
|
||||
dhw = self.coordinator.data.dhw
|
||||
assert dhw is not None
|
||||
return dhw
|
||||
|
||||
@property
|
||||
def current_operation(self) -> str | None:
|
||||
"""Return current operation."""
|
||||
if (
|
||||
operating_mode := self._dhw.operating_mode
|
||||
operating_mode := self.coordinator.data.dhw.operating_mode
|
||||
) is None or operating_mode.value is None:
|
||||
return None
|
||||
return BSBLAN_TO_HA_OPERATION_MODE.get(operating_mode.value)
|
||||
@@ -131,14 +119,16 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
if (current_temp := self._dhw.dhw_actual_value_top_temperature) is None:
|
||||
if (
|
||||
current_temp := self.coordinator.data.dhw.dhw_actual_value_top_temperature
|
||||
) is None:
|
||||
return None
|
||||
return current_temp.value
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
if (target_temp := self._dhw.nominal_setpoint) is None:
|
||||
if (target_temp := self.coordinator.data.dhw.nominal_setpoint) is None:
|
||||
return None
|
||||
return target_temp.value
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
},
|
||||
"services": {
|
||||
"press": {
|
||||
"description": "Presses a button.",
|
||||
"name": "Press button"
|
||||
"description": "Presses a button entity.",
|
||||
"name": "Press"
|
||||
}
|
||||
},
|
||||
"title": "Button",
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
"""Provides conditions for calendars."""
|
||||
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_event_active": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the calendar conditions."""
|
||||
return CONDITIONS
|
||||
@@ -1,14 +0,0 @@
|
||||
is_event_active:
|
||||
target:
|
||||
entity:
|
||||
- domain: calendar
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_event_active": {
|
||||
"condition": "mdi:calendar-check"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:calendar",
|
||||
|
||||
@@ -1,18 +1,4 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if"
|
||||
},
|
||||
"conditions": {
|
||||
"is_event_active": {
|
||||
"description": "Tests if one or more calendars have an active event.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::calendar::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Calendar event is active"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::calendar::title%]",
|
||||
@@ -60,12 +46,6 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_offset_type": {
|
||||
"options": {
|
||||
"after": "After",
|
||||
@@ -110,7 +90,7 @@
|
||||
"name": "Summary"
|
||||
}
|
||||
},
|
||||
"name": "Create calendar event"
|
||||
"name": "Create event"
|
||||
},
|
||||
"get_events": {
|
||||
"description": "Retrieves events on a calendar within a time range.",
|
||||
@@ -128,7 +108,7 @@
|
||||
"name": "Start time"
|
||||
}
|
||||
},
|
||||
"name": "Get calendar events"
|
||||
"name": "Get events"
|
||||
}
|
||||
},
|
||||
"title": "Calendar",
|
||||
|
||||
@@ -51,11 +51,11 @@
|
||||
"services": {
|
||||
"disable_motion_detection": {
|
||||
"description": "Disables the motion detection of a camera.",
|
||||
"name": "Disable camera motion detection"
|
||||
"name": "Disable motion detection"
|
||||
},
|
||||
"enable_motion_detection": {
|
||||
"description": "Enables the motion detection of a camera.",
|
||||
"name": "Enable camera motion detection"
|
||||
"name": "Enable motion detection"
|
||||
},
|
||||
"play_stream": {
|
||||
"description": "Plays a camera stream on a supported media player.",
|
||||
@@ -69,7 +69,7 @@
|
||||
"name": "Media player"
|
||||
}
|
||||
},
|
||||
"name": "Play camera stream"
|
||||
"name": "Play stream"
|
||||
},
|
||||
"record": {
|
||||
"description": "Creates a recording of a live camera feed.",
|
||||
@@ -87,7 +87,7 @@
|
||||
"name": "Lookback"
|
||||
}
|
||||
},
|
||||
"name": "Record camera feed"
|
||||
"name": "Record"
|
||||
},
|
||||
"snapshot": {
|
||||
"description": "Takes a snapshot from a camera.",
|
||||
@@ -97,15 +97,15 @@
|
||||
"name": "Filename"
|
||||
}
|
||||
},
|
||||
"name": "Take camera snapshot"
|
||||
"name": "Take snapshot"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Turns off a camera.",
|
||||
"name": "Turn off camera"
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Turns on a camera.",
|
||||
"name": "Turn on camera"
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
"title": "Camera"
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
|
||||
PLATFORMS: list[Platform] = [Platform.LIGHT]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
"""Casper Glow integration binary sensor platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
from .entity import CasperGlowEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: CasperGlowConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the binary sensor platform for Casper Glow."""
|
||||
async_add_entities([CasperGlowPausedBinarySensor(entry.runtime_data)])
|
||||
|
||||
|
||||
class CasperGlowPausedBinarySensor(CasperGlowEntity, BinarySensorEntity):
|
||||
"""Binary sensor indicating whether the Casper Glow dimming is paused."""
|
||||
|
||||
_attr_translation_key = "paused"
|
||||
|
||||
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
|
||||
"""Initialize the paused binary sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_paused"
|
||||
if coordinator.device.state.is_paused is not None:
|
||||
self._attr_is_on = coordinator.device.state.is_paused
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register state update callback when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self._device.register_callback(self._async_handle_state_update)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_handle_state_update(self, state: GlowState) -> None:
|
||||
"""Handle a state update from the device."""
|
||||
if state.is_paused is not None:
|
||||
self._attr_is_on = state.is_paused
|
||||
self.async_write_ha_state()
|
||||
@@ -1,73 +0,0 @@
|
||||
"""Casper Glow integration button platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pycasperglow import CasperGlow
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
from .entity import CasperGlowEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class CasperGlowButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describe a Casper Glow button entity."""
|
||||
|
||||
press_fn: Callable[[CasperGlow], Awaitable[None]]
|
||||
|
||||
|
||||
BUTTON_DESCRIPTIONS: tuple[CasperGlowButtonEntityDescription, ...] = (
|
||||
CasperGlowButtonEntityDescription(
|
||||
key="pause",
|
||||
translation_key="pause",
|
||||
press_fn=lambda device: device.pause(),
|
||||
),
|
||||
CasperGlowButtonEntityDescription(
|
||||
key="resume",
|
||||
translation_key="resume",
|
||||
press_fn=lambda device: device.resume(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: CasperGlowConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the button platform for Casper Glow."""
|
||||
async_add_entities(
|
||||
CasperGlowButton(entry.runtime_data, description)
|
||||
for description in BUTTON_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class CasperGlowButton(CasperGlowEntity, ButtonEntity):
|
||||
"""A Casper Glow button entity."""
|
||||
|
||||
entity_description: CasperGlowButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: CasperGlowCoordinator,
|
||||
description: CasperGlowButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a Casper Glow button."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = (
|
||||
f"{format_mac(coordinator.device.address)}_{description.key}"
|
||||
)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
await self._async_command(self.entity_description.press_fn(self._device))
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"paused": {
|
||||
"default": "mdi:timer-pause"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"pause": {
|
||||
"default": "mdi:pause"
|
||||
},
|
||||
"resume": {
|
||||
"default": "mdi:play"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pycasperglow"],
|
||||
"quality_scale": "silver",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pycasperglow==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -32,9 +32,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: Bluetooth device with no authentication credentials.
|
||||
reauthentication-flow: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
@@ -55,9 +53,15 @@ rules:
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: No entity translations needed.
|
||||
exception-translations:
|
||||
status: exempt
|
||||
comment: No custom services that raise exceptions.
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: No icon translations needed.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
@@ -26,21 +26,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"paused": {
|
||||
"name": "Dimming paused"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"pause": {
|
||||
"name": "Pause dimming"
|
||||
},
|
||||
"resume": {
|
||||
"name": "Resume dimming"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"communication_error": {
|
||||
"message": "An error occurred while communicating with the Casper Glow: {error}"
|
||||
|
||||
@@ -30,7 +30,6 @@ class ChessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
client = ChessComClient(session=session)
|
||||
try:
|
||||
user = await client.get_player(user_input[CONF_USERNAME])
|
||||
await client.get_player_stats(user_input[CONF_USERNAME])
|
||||
except NotFoundError:
|
||||
errors["base"] = "player_not_found"
|
||||
except Exception:
|
||||
@@ -39,9 +38,7 @@ class ChessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
await self.async_set_unique_id(str(user.player_id))
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=user.name or user.username, data=user_input
|
||||
)
|
||||
return self.async_create_entry(title=user.name, data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
|
||||
@@ -1,68 +1,20 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"chess960_daily_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess960_daily_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess960_daily_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess960_daily_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_blitz_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_blitz_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_blitz_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess_blitz_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_bullet_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_bullet_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_bullet_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess_bullet_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_daily_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_daily_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_daily_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess_daily_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_rapid_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_rapid_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_rapid_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess_rapid_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"followers": {
|
||||
"default": "mdi:account-multiple"
|
||||
},
|
||||
"total_daily_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"total_daily_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"total_daily_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from chess_com_api import PlayerStats
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
@@ -27,14 +24,7 @@ class ChessEntityDescription(SensorEntityDescription):
|
||||
value_fn: Callable[[ChessData], float]
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class ChessModeEntityDescription(SensorEntityDescription):
|
||||
"""Sensor description for a Chess.com game mode."""
|
||||
|
||||
value_fn: Callable[[dict[str, Any]], float]
|
||||
|
||||
|
||||
PLAYER_SENSORS: tuple[ChessEntityDescription, ...] = (
|
||||
SENSORS: tuple[ChessEntityDescription, ...] = (
|
||||
ChessEntityDescription(
|
||||
key="followers",
|
||||
translation_key="followers",
|
||||
@@ -43,46 +33,35 @@ PLAYER_SENSORS: tuple[ChessEntityDescription, ...] = (
|
||||
value_fn=lambda state: state.player.followers,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
GAME_MODE_SENSORS: tuple[ChessModeEntityDescription, ...] = (
|
||||
ChessModeEntityDescription(
|
||||
key="rating",
|
||||
translation_key="rating",
|
||||
ChessEntityDescription(
|
||||
key="chess_daily_rating",
|
||||
translation_key="chess_daily_rating",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda mode: mode["last"]["rating"],
|
||||
value_fn=lambda state: state.stats.chess_daily["last"]["rating"],
|
||||
),
|
||||
ChessModeEntityDescription(
|
||||
key="won",
|
||||
translation_key="won",
|
||||
ChessEntityDescription(
|
||||
key="total_daily_won",
|
||||
translation_key="total_daily_won",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda mode: mode["record"]["win"],
|
||||
value_fn=lambda state: state.stats.chess_daily["record"]["win"],
|
||||
),
|
||||
ChessModeEntityDescription(
|
||||
key="lost",
|
||||
translation_key="lost",
|
||||
ChessEntityDescription(
|
||||
key="total_daily_lost",
|
||||
translation_key="total_daily_lost",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda mode: mode["record"]["loss"],
|
||||
value_fn=lambda state: state.stats.chess_daily["record"]["loss"],
|
||||
),
|
||||
ChessModeEntityDescription(
|
||||
key="draw",
|
||||
translation_key="draw",
|
||||
ChessEntityDescription(
|
||||
key="total_daily_draw",
|
||||
translation_key="total_daily_draw",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda mode: mode["record"]["draw"],
|
||||
value_fn=lambda state: state.stats.chess_daily["record"]["draw"],
|
||||
),
|
||||
)
|
||||
|
||||
GAME_MODES: dict[str, Callable[[PlayerStats], dict[str, Any] | None]] = {
|
||||
"chess_daily": lambda stats: stats.chess_daily,
|
||||
"chess_rapid": lambda stats: stats.chess_rapid,
|
||||
"chess_bullet": lambda stats: stats.chess_bullet,
|
||||
"chess_blitz": lambda stats: stats.chess_blitz,
|
||||
"chess960_daily": lambda stats: stats.chess960_daily,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -92,22 +71,13 @@ async def async_setup_entry(
|
||||
"""Initialize the entries."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities: list[SensorEntity] = [
|
||||
ChessPlayerSensor(coordinator, description) for description in PLAYER_SENSORS
|
||||
]
|
||||
|
||||
for game_mode, stats_fn in GAME_MODES.items():
|
||||
if stats_fn(coordinator.data.stats) is not None:
|
||||
entities.extend(
|
||||
ChessGameModeSensor(coordinator, description, game_mode, stats_fn)
|
||||
for description in GAME_MODE_SENSORS
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(
|
||||
ChessPlayerSensor(coordinator, description) for description in SENSORS
|
||||
)
|
||||
|
||||
|
||||
class ChessPlayerSensor(ChessEntity, SensorEntity):
|
||||
"""Chess.com player sensor."""
|
||||
"""Chess.com sensor."""
|
||||
|
||||
entity_description: ChessEntityDescription
|
||||
|
||||
@@ -125,33 +95,3 @@ class ChessPlayerSensor(ChessEntity, SensorEntity):
|
||||
def native_value(self) -> float:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
|
||||
class ChessGameModeSensor(ChessEntity, SensorEntity):
|
||||
"""Chess.com game mode sensor."""
|
||||
|
||||
entity_description: ChessModeEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ChessCoordinator,
|
||||
description: ChessModeEntityDescription,
|
||||
game_mode: str,
|
||||
stats_fn: Callable[[PlayerStats], dict[str, Any] | None],
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._stats_fn = stats_fn
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.config_entry.unique_id}.{game_mode}.{description.key}"
|
||||
)
|
||||
self._attr_translation_key = f"{game_mode}_{description.translation_key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the state of the sensor."""
|
||||
mode_data = self._stats_fn(self.coordinator.data.stats)
|
||||
if TYPE_CHECKING:
|
||||
assert mode_data is not None
|
||||
return self.entity_description.value_fn(mode_data)
|
||||
|
||||
@@ -23,84 +23,24 @@
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"chess960_daily_draw": {
|
||||
"name": "Total daily Chess960 games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess960_daily_lost": {
|
||||
"name": "Total daily Chess960 games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess960_daily_rating": {
|
||||
"name": "Daily Chess960 rating"
|
||||
},
|
||||
"chess960_daily_won": {
|
||||
"name": "Total daily Chess960 games won",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_blitz_draw": {
|
||||
"name": "Total blitz chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_blitz_lost": {
|
||||
"name": "Total blitz chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_blitz_rating": {
|
||||
"name": "Blitz chess rating"
|
||||
},
|
||||
"chess_blitz_won": {
|
||||
"name": "Total blitz chess games won",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_bullet_draw": {
|
||||
"name": "Total bullet chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_bullet_lost": {
|
||||
"name": "Total bullet chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_bullet_rating": {
|
||||
"name": "Bullet chess rating"
|
||||
},
|
||||
"chess_bullet_won": {
|
||||
"name": "Total bullet chess games won",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_daily_draw": {
|
||||
"name": "Total daily chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_daily_lost": {
|
||||
"name": "Total daily chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_daily_rating": {
|
||||
"name": "Daily chess rating"
|
||||
},
|
||||
"chess_daily_won": {
|
||||
"name": "Total daily chess games won",
|
||||
"unit_of_measurement": "games"
|
||||
},
|
||||
"chess_rapid_draw": {
|
||||
"name": "Total rapid chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_rapid_lost": {
|
||||
"name": "Total rapid chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_rapid_rating": {
|
||||
"name": "Rapid chess rating"
|
||||
},
|
||||
"chess_rapid_won": {
|
||||
"name": "Total rapid chess games won",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"followers": {
|
||||
"name": "Followers",
|
||||
"unit_of_measurement": "followers"
|
||||
},
|
||||
"total_daily_draw": {
|
||||
"name": "Total chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::total_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"total_daily_lost": {
|
||||
"name": "Total chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::total_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"total_daily_won": {
|
||||
"name": "Total chess games won",
|
||||
"unit_of_measurement": "games"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
"""Provides conditions for climates."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
|
||||
Condition,
|
||||
ConditionConfig,
|
||||
EntityConditionBase,
|
||||
EntityNumericalConditionWithUnitBase,
|
||||
make_entity_numerical_condition,
|
||||
make_entity_state_condition,
|
||||
@@ -21,42 +13,12 @@ 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: DomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_unit_converter = TemperatureConverter
|
||||
|
||||
def _get_entity_unit(self, entity_state: State) -> str | None:
|
||||
@@ -66,7 +28,6 @@ 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,
|
||||
@@ -89,7 +50,7 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
|
||||
),
|
||||
"target_humidity": make_entity_numerical_condition(
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"target_temperature": ClimateTargetTemperatureCondition,
|
||||
|
||||
@@ -13,31 +13,58 @@
|
||||
- all
|
||||
- any
|
||||
|
||||
.humidity_threshold_entity: &humidity_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
device_class: humidity
|
||||
- domain: number
|
||||
device_class: humidity
|
||||
.number_or_entity_humidity: &number_or_entity_humidity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
device_class: humidity
|
||||
- domain: number
|
||||
device_class: humidity
|
||||
translation_key: number_or_entity
|
||||
|
||||
.humidity_threshold_number: &humidity_threshold_number
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
.number_or_entity_temperature: &number_or_entity_temperature
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "°C"
|
||||
- "°F"
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
translation_key: number_or_entity
|
||||
|
||||
.temperature_units: &temperature_units
|
||||
- "°C"
|
||||
- "°F"
|
||||
|
||||
.temperature_threshold_entity: &temperature_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *temperature_units
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
.condition_unit_temperature: &condition_unit_temperature
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "°C"
|
||||
- "°F"
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
@@ -45,43 +72,17 @@ 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:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *humidity_threshold_entity
|
||||
mode: is
|
||||
number: *humidity_threshold_number
|
||||
above: *number_or_entity_humidity
|
||||
below: *number_or_entity_humidity
|
||||
|
||||
target_temperature:
|
||||
target: *condition_climate_target
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *temperature_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *temperature_units
|
||||
above: *number_or_entity_temperature
|
||||
below: *number_or_entity_temperature
|
||||
unit: *condition_unit_temperature
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
"is_heating": {
|
||||
"condition": "mdi:fire"
|
||||
},
|
||||
"is_hvac_mode": {
|
||||
"condition": "mdi:thermostat"
|
||||
},
|
||||
"is_off": {
|
||||
"condition": "mdi:power-off"
|
||||
},
|
||||
|
||||
@@ -11,8 +11,7 @@ set_preset_mode:
|
||||
required: true
|
||||
example: "away"
|
||||
selector:
|
||||
state:
|
||||
attribute: preset_mode
|
||||
text:
|
||||
|
||||
set_temperature:
|
||||
target:
|
||||
@@ -56,10 +55,16 @@ set_temperature:
|
||||
mode: box
|
||||
hvac_mode:
|
||||
selector:
|
||||
state:
|
||||
hide_states:
|
||||
- unavailable
|
||||
- unknown
|
||||
select:
|
||||
options:
|
||||
- "off"
|
||||
- "auto"
|
||||
- "cool"
|
||||
- "dry"
|
||||
- "fan_only"
|
||||
- "heat_cool"
|
||||
- "heat"
|
||||
translation_key: hvac_mode
|
||||
set_humidity:
|
||||
target:
|
||||
entity:
|
||||
@@ -86,8 +91,7 @@ set_fan_mode:
|
||||
required: true
|
||||
example: "low"
|
||||
selector:
|
||||
state:
|
||||
attribute: fan_mode
|
||||
text:
|
||||
|
||||
set_hvac_mode:
|
||||
target:
|
||||
@@ -111,8 +115,7 @@ set_swing_mode:
|
||||
required: true
|
||||
example: "on"
|
||||
selector:
|
||||
state:
|
||||
attribute: swing_mode
|
||||
text:
|
||||
|
||||
set_swing_horizontal_mode:
|
||||
target:
|
||||
@@ -125,8 +128,7 @@ set_swing_horizontal_mode:
|
||||
required: true
|
||||
example: "on"
|
||||
selector:
|
||||
state:
|
||||
attribute: swing_horizontal_mode
|
||||
text:
|
||||
|
||||
turn_on:
|
||||
target:
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_threshold_name": "Threshold type"
|
||||
"condition_behavior_description": "How the state should match on the targeted climate-control devices.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted climates to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_cooling": {
|
||||
"description": "Tests if one or more climate-control devices are cooling.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -19,6 +20,7 @@
|
||||
"description": "Tests if one or more climate-control devices are drying.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -28,28 +30,17 @@
|
||||
"description": "Tests if one or more climate-control devices are heating.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"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": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -59,6 +50,7 @@
|
||||
"description": "Tests if one or more climate-control devices are on.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -67,11 +59,17 @@
|
||||
"target_humidity": {
|
||||
"description": "Tests the humidity setpoint of one or more climate-control devices.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Require the target humidity to be above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::climate::common::condition_threshold_name%]"
|
||||
"below": {
|
||||
"description": "Require the target humidity to be below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target humidity"
|
||||
@@ -79,11 +77,21 @@
|
||||
"target_temperature": {
|
||||
"description": "Tests the temperature setpoint of one or more climate-control devices.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Require the target temperature to be above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::climate::common::condition_threshold_name%]"
|
||||
"below": {
|
||||
"description": "Require the target temperature to be below this value.",
|
||||
"name": "Below"
|
||||
},
|
||||
"unit": {
|
||||
"description": "All values will be converted to this unit when evaluating the condition.",
|
||||
"name": "Unit of measurement"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target temperature"
|
||||
@@ -273,77 +281,102 @@
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"hvac_mode": {
|
||||
"options": {
|
||||
"auto": "[%key:common::state::auto%]",
|
||||
"cool": "Cool",
|
||||
"dry": "Dry",
|
||||
"fan_only": "Fan only",
|
||||
"heat": "Heat",
|
||||
"heat_cool": "Heat/cool",
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
"number": "Number"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
},
|
||||
"trigger_threshold_type": {
|
||||
"options": {
|
||||
"above": "Above a value",
|
||||
"below": "Below a value",
|
||||
"between": "In a range",
|
||||
"outside": "Outside a range"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"set_fan_mode": {
|
||||
"description": "Sets the fan mode of a climate-control device.",
|
||||
"description": "Sets fan operation mode.",
|
||||
"fields": {
|
||||
"fan_mode": {
|
||||
"description": "Fan operation mode.",
|
||||
"name": "Fan mode"
|
||||
}
|
||||
},
|
||||
"name": "Set climate-control device fan mode"
|
||||
"name": "Set fan mode"
|
||||
},
|
||||
"set_humidity": {
|
||||
"description": "Sets the target humidity of a climate-control device.",
|
||||
"description": "Sets target humidity.",
|
||||
"fields": {
|
||||
"humidity": {
|
||||
"description": "Target humidity.",
|
||||
"name": "Humidity"
|
||||
}
|
||||
},
|
||||
"name": "Set climate-control device target humidity"
|
||||
"name": "Set target humidity"
|
||||
},
|
||||
"set_hvac_mode": {
|
||||
"description": "Sets the HVAC mode of a climate-control device.",
|
||||
"description": "Sets HVAC operation mode.",
|
||||
"fields": {
|
||||
"hvac_mode": {
|
||||
"description": "HVAC operation mode.",
|
||||
"name": "HVAC mode"
|
||||
}
|
||||
},
|
||||
"name": "Set climate-control device HVAC mode"
|
||||
"name": "Set HVAC mode"
|
||||
},
|
||||
"set_preset_mode": {
|
||||
"description": "Sets the preset mode of a climate-control device.",
|
||||
"description": "Sets preset mode.",
|
||||
"fields": {
|
||||
"preset_mode": {
|
||||
"description": "Preset mode.",
|
||||
"name": "Preset mode"
|
||||
}
|
||||
},
|
||||
"name": "Set climate-control device preset mode"
|
||||
"name": "Set preset mode"
|
||||
},
|
||||
"set_swing_horizontal_mode": {
|
||||
"description": "Sets the horizontal swing mode of a climate-control device.",
|
||||
"description": "Sets horizontal swing operation mode.",
|
||||
"fields": {
|
||||
"swing_horizontal_mode": {
|
||||
"description": "Horizontal swing operation mode.",
|
||||
"name": "Horizontal swing mode"
|
||||
}
|
||||
},
|
||||
"name": "Set climate-control device horizontal swing mode"
|
||||
"name": "Set horizontal swing mode"
|
||||
},
|
||||
"set_swing_mode": {
|
||||
"description": "Sets the swing mode of a climate-control device.",
|
||||
"description": "Sets swing operation mode.",
|
||||
"fields": {
|
||||
"swing_mode": {
|
||||
"description": "Swing operation mode.",
|
||||
"name": "Swing mode"
|
||||
}
|
||||
},
|
||||
"name": "Set climate-control device swing mode"
|
||||
"name": "Set swing mode"
|
||||
},
|
||||
"set_temperature": {
|
||||
"description": "Sets the target temperature of a climate-control device.",
|
||||
"description": "Sets the temperature setpoint.",
|
||||
"fields": {
|
||||
"hvac_mode": {
|
||||
"description": "HVAC operation mode.",
|
||||
@@ -362,19 +395,19 @@
|
||||
"name": "Target temperature"
|
||||
}
|
||||
},
|
||||
"name": "Set climate-control device target temperature"
|
||||
"name": "Set target temperature"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Toggles a climate-control device on/off.",
|
||||
"name": "Toggle climate-control device"
|
||||
"description": "Toggles climate device, from on to off, or off to on.",
|
||||
"name": "[%key:common::action::toggle%]"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Turns off a climate-control device.",
|
||||
"name": "Turn off climate-control device"
|
||||
"description": "Turns climate device off.",
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Turns on a climate-control device.",
|
||||
"name": "Turn on climate-control device"
|
||||
"description": "Turns climate device on.",
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
"title": "Climate",
|
||||
@@ -383,6 +416,7 @@
|
||||
"description": "Triggers after the mode of one or more climate-control devices changes.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
},
|
||||
"hvac_mode": {
|
||||
@@ -396,6 +430,7 @@
|
||||
"description": "Triggers after one or more climate-control devices start cooling.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -405,6 +440,7 @@
|
||||
"description": "Triggers after one or more climate-control devices start drying.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -414,6 +450,7 @@
|
||||
"description": "Triggers after one or more climate-control devices start heating.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -422,8 +459,13 @@
|
||||
"target_humidity_changed": {
|
||||
"description": "Triggers after the humidity setpoint of one or more climate-control devices changes.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::climate::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Trigger when the target humidity is above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Trigger when the target humidity is below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target humidity changed"
|
||||
@@ -432,10 +474,20 @@
|
||||
"description": "Triggers after the humidity setpoint of one or more climate-control devices crosses a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::climate::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "Lower threshold limit.",
|
||||
"name": "Lower threshold"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "Type of threshold crossing to trigger on.",
|
||||
"name": "Threshold type"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "Upper threshold limit.",
|
||||
"name": "Upper threshold"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target humidity crossed threshold"
|
||||
@@ -443,8 +495,17 @@
|
||||
"target_temperature_changed": {
|
||||
"description": "Triggers after the temperature setpoint of one or more climate-control devices changes.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::climate::common::trigger_threshold_name%]"
|
||||
"above": {
|
||||
"description": "Trigger when the target temperature is above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Trigger when the target temperature is below this value.",
|
||||
"name": "Below"
|
||||
},
|
||||
"unit": {
|
||||
"description": "All values will be converted to this unit when evaluating the trigger.",
|
||||
"name": "Unit of measurement"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target temperature changed"
|
||||
@@ -453,10 +514,24 @@
|
||||
"description": "Triggers after the temperature setpoint of one or more climate-control devices crosses a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::climate::common::trigger_threshold_name%]"
|
||||
"lower_limit": {
|
||||
"description": "Lower threshold limit.",
|
||||
"name": "Lower threshold"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "Type of threshold crossing to trigger on.",
|
||||
"name": "Threshold type"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::climate::triggers::target_temperature_changed::fields::unit::description%]",
|
||||
"name": "[%key:component::climate::triggers::target_temperature_changed::fields::unit::name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "Upper threshold limit.",
|
||||
"name": "Upper threshold"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target temperature crossed threshold"
|
||||
@@ -465,6 +540,7 @@
|
||||
"description": "Triggers after one or more climate-control devices turn off.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -474,6 +550,7 @@
|
||||
"description": "Triggers after one or more climate-control devices turn on, regardless of the mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
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: DomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_domain_specs = {DOMAIN: NumericalDomainSpec(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: DomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"target_temperature_changed": ClimateTargetTemperatureChangedTrigger,
|
||||
|
||||
@@ -14,31 +14,70 @@
|
||||
- last
|
||||
- any
|
||||
|
||||
.humidity_threshold_entity: &humidity_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
device_class: humidity
|
||||
- domain: number
|
||||
device_class: humidity
|
||||
.number_or_entity_humidity: &number_or_entity_humidity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
device_class: humidity
|
||||
- domain: number
|
||||
device_class: humidity
|
||||
translation_key: number_or_entity
|
||||
|
||||
.humidity_threshold_number: &humidity_threshold_number
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
.number_or_entity_temperature: &number_or_entity_temperature
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "°C"
|
||||
- "°F"
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
translation_key: number_or_entity
|
||||
|
||||
.temperature_units: &temperature_units
|
||||
- "°C"
|
||||
- "°F"
|
||||
.trigger_unit_temperature: &trigger_unit_temperature
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "°C"
|
||||
- "°F"
|
||||
|
||||
.temperature_threshold_entity: &temperature_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *temperature_units
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- above
|
||||
- below
|
||||
- between
|
||||
- outside
|
||||
translation_key: trigger_threshold_type
|
||||
|
||||
started_cooling: *trigger_common
|
||||
started_drying: *trigger_common
|
||||
@@ -64,49 +103,29 @@ hvac_mode_changed:
|
||||
target_humidity_changed:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *humidity_threshold_entity
|
||||
mode: changed
|
||||
number: *humidity_threshold_number
|
||||
above: *number_or_entity_humidity
|
||||
below: *number_or_entity_humidity
|
||||
|
||||
target_humidity_crossed_threshold:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *humidity_threshold_entity
|
||||
mode: crossed
|
||||
number: *humidity_threshold_number
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_humidity
|
||||
upper_limit: *number_or_entity_humidity
|
||||
|
||||
target_temperature_changed:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *temperature_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *temperature_units
|
||||
above: *number_or_entity_temperature
|
||||
below: *number_or_entity_temperature
|
||||
unit: *trigger_unit_temperature
|
||||
|
||||
target_temperature_crossed_threshold:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *temperature_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *temperature_units
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_temperature
|
||||
upper_limit: *number_or_entity_temperature
|
||||
unit: *trigger_unit_temperature
|
||||
|
||||
@@ -75,11 +75,11 @@
|
||||
"services": {
|
||||
"remote_connect": {
|
||||
"description": "Makes the instance UI accessible from outside of the local network by enabling your Home Assistant Cloud connection.",
|
||||
"name": "Enable Home Assistant Cloud remote access"
|
||||
"name": "Enable remote access"
|
||||
},
|
||||
"remote_disconnect": {
|
||||
"description": "Disconnects the instance UI from Home Assistant Cloud. This disables access to it from outside your local network.",
|
||||
"name": "Disable Home Assistant Cloud remote access"
|
||||
"name": "Disable remote access"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
|
||||
@@ -112,7 +112,7 @@ class ComelitAlarmEntity(
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if alarm is available."""
|
||||
if self._area.human_status == AlarmAreaState.UNKNOWN:
|
||||
if self._area.human_status in [AlarmAreaState.ANOMALY, AlarmAreaState.UNKNOWN]:
|
||||
return False
|
||||
return super().available
|
||||
|
||||
@@ -151,7 +151,7 @@ class ComelitAlarmEntity(
|
||||
if code != str(self.coordinator.api.device_pin):
|
||||
return
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[DISABLE], self._area.anomaly
|
||||
self._area.index, ALARM_ACTIONS[DISABLE]
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.DISARMED, ALARM_AREA_ARMED_STATUS[DISABLE]
|
||||
@@ -160,7 +160,7 @@ class ComelitAlarmEntity(
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[AWAY], self._area.anomaly
|
||||
self._area.index, ALARM_ACTIONS[AWAY]
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[AWAY]
|
||||
@@ -169,7 +169,7 @@ class ComelitAlarmEntity(
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[HOME], self._area.anomaly
|
||||
self._area.index, ALARM_ACTIONS[HOME]
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[HOME_P1]
|
||||
@@ -178,7 +178,7 @@ class ComelitAlarmEntity(
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[NIGHT], self._area.anomaly
|
||||
self._area.index, ALARM_ACTIONS[NIGHT]
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT]
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==2.0.2"]
|
||||
"requirements": ["aiocomelit==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"services": {
|
||||
"process": {
|
||||
"description": "Sends text to a conversation agent for processing.",
|
||||
"description": "Launches a conversation from a transcribed text.",
|
||||
"fields": {
|
||||
"agent_id": {
|
||||
"description": "Conversation agent to process your request. The conversation agent is the brains of your assistant. It processes the incoming text commands.",
|
||||
@@ -25,10 +25,10 @@
|
||||
"name": "Text"
|
||||
}
|
||||
},
|
||||
"name": "Process conversation"
|
||||
"name": "Process"
|
||||
},
|
||||
"reload": {
|
||||
"description": "Reloads the intent configuration of conversation agents.",
|
||||
"description": "Reloads the intent configuration.",
|
||||
"fields": {
|
||||
"agent_id": {
|
||||
"description": "Conversation agent to reload.",
|
||||
@@ -39,7 +39,7 @@
|
||||
"name": "[%key:common::config_flow::data::language%]"
|
||||
}
|
||||
},
|
||||
"name": "Reload conversation agents"
|
||||
"name": "[%key:common::action::reload%]"
|
||||
}
|
||||
},
|
||||
"title": "Conversation"
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
"""Provides conditions for counters."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition, make_entity_numerical_condition
|
||||
|
||||
DOMAIN = "counter"
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_value": make_entity_numerical_condition(DOMAIN),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the conditions for counters."""
|
||||
return CONDITIONS
|
||||
@@ -1,25 +0,0 @@
|
||||
is_value:
|
||||
target:
|
||||
entity:
|
||||
- domain: counter
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity:
|
||||
- domain: counter
|
||||
- domain: input_number
|
||||
- domain: number
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_value": {
|
||||
"condition": "mdi:counter"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"decrement": {
|
||||
"service": "mdi:numeric-negative-1"
|
||||
@@ -17,22 +12,5 @@
|
||||
"set_value": {
|
||||
"service": "mdi:counter"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"decremented": {
|
||||
"trigger": "mdi:numeric-negative-1"
|
||||
},
|
||||
"incremented": {
|
||||
"trigger": "mdi:numeric-positive-1"
|
||||
},
|
||||
"maximum_reached": {
|
||||
"trigger": "mdi:sort-numeric-ascending-variant"
|
||||
},
|
||||
"minimum_reached": {
|
||||
"trigger": "mdi:sort-numeric-descending-variant"
|
||||
},
|
||||
"reset": {
|
||||
"trigger": "mdi:refresh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,4 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_name": "Trigger when"
|
||||
},
|
||||
"conditions": {
|
||||
"is_value": {
|
||||
"description": "Tests the value of one or more counters.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "Condition passes if"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "Threshold type"
|
||||
}
|
||||
},
|
||||
"name": "Counter value"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::counter::title%]",
|
||||
@@ -42,81 +25,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"decrement": {
|
||||
"description": "Decrements a counter by its step size.",
|
||||
"name": "Decrement counter"
|
||||
"name": "Decrement"
|
||||
},
|
||||
"increment": {
|
||||
"description": "Increments a counter by its step size.",
|
||||
"name": "Increment counter"
|
||||
"name": "Increment"
|
||||
},
|
||||
"reset": {
|
||||
"description": "Resets a counter to its initial value.",
|
||||
"name": "Reset counter"
|
||||
"name": "Reset"
|
||||
},
|
||||
"set_value": {
|
||||
"description": "Sets a counter to a specific value.",
|
||||
"description": "Sets the counter to a specific value.",
|
||||
"fields": {
|
||||
"value": {
|
||||
"description": "The new counter value the entity should be set to.",
|
||||
"name": "Value"
|
||||
}
|
||||
},
|
||||
"name": "Set counter value"
|
||||
"name": "Set"
|
||||
}
|
||||
},
|
||||
"title": "Counter",
|
||||
"triggers": {
|
||||
"decremented": {
|
||||
"description": "Triggers after one or more counters decrement.",
|
||||
"name": "Counter decremented"
|
||||
},
|
||||
"incremented": {
|
||||
"description": "Triggers after one or more counters increment.",
|
||||
"name": "Counter incremented"
|
||||
},
|
||||
"maximum_reached": {
|
||||
"description": "Triggers after one or more counters reach their maximum value.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::counter::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Counter reached maximum"
|
||||
},
|
||||
"minimum_reached": {
|
||||
"description": "Triggers after one or more counters reach their minimum value.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::counter::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Counter reached minimum"
|
||||
},
|
||||
"reset": {
|
||||
"description": "Triggers after one or more counters are reset.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::counter::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Counter reset"
|
||||
}
|
||||
}
|
||||
"title": "Counter"
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
"""Provides triggers for counters."""
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_MAXIMUM,
|
||||
CONF_MINIMUM,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
||||
EntityTriggerBase,
|
||||
Trigger,
|
||||
)
|
||||
|
||||
from . import CONF_INITIAL, DOMAIN
|
||||
|
||||
|
||||
def _is_integer_state(state: State) -> bool:
|
||||
"""Return True if the state's value can be interpreted as an integer."""
|
||||
try:
|
||||
int(state.state)
|
||||
except TypeError, ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class CounterBaseIntegerTrigger(EntityTriggerBase):
|
||||
"""Base trigger for valid counter integer states."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
_schema = ENTITY_STATE_TRIGGER_SCHEMA
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state is valid."""
|
||||
return _is_integer_state(state)
|
||||
|
||||
|
||||
class CounterDecrementedTrigger(CounterBaseIntegerTrigger):
|
||||
"""Trigger for when a counter is decremented."""
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and the state has changed."""
|
||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return False
|
||||
return int(from_state.state) > int(to_state.state)
|
||||
|
||||
|
||||
class CounterIncrementedTrigger(CounterBaseIntegerTrigger):
|
||||
"""Trigger for when a counter is incremented."""
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and the state has changed."""
|
||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return False
|
||||
return int(from_state.state) < int(to_state.state)
|
||||
|
||||
|
||||
class CounterValueBaseTrigger(EntityTriggerBase):
|
||||
"""Base trigger for counter value changes."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and the state has changed."""
|
||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return False
|
||||
return from_state.state != to_state.state
|
||||
|
||||
|
||||
class CounterMaxReachedTrigger(CounterValueBaseTrigger):
|
||||
"""Trigger for when a counter reaches its maximum value."""
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state matches the expected state(s)."""
|
||||
if (max_value := state.attributes.get(CONF_MAXIMUM)) is None:
|
||||
return False
|
||||
return state.state == str(max_value)
|
||||
|
||||
|
||||
class CounterMinReachedTrigger(CounterValueBaseTrigger):
|
||||
"""Trigger for when a counter reaches its minimum value."""
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state matches the expected state(s)."""
|
||||
if (min_value := state.attributes.get(CONF_MINIMUM)) is None:
|
||||
return False
|
||||
return state.state == str(min_value)
|
||||
|
||||
|
||||
class CounterResetTrigger(CounterValueBaseTrigger):
|
||||
"""Trigger for reset of counter entities."""
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state matches the expected state(s)."""
|
||||
if (init_state := state.attributes.get(CONF_INITIAL)) is None:
|
||||
return False
|
||||
return state.state == str(init_state)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"decremented": CounterDecrementedTrigger,
|
||||
"incremented": CounterIncrementedTrigger,
|
||||
"maximum_reached": CounterMaxReachedTrigger,
|
||||
"minimum_reached": CounterMinReachedTrigger,
|
||||
"reset": CounterResetTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for counters."""
|
||||
return TRIGGERS
|
||||
@@ -1,27 +0,0 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: counter
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
incremented:
|
||||
target:
|
||||
entity:
|
||||
domain: counter
|
||||
decremented:
|
||||
target:
|
||||
entity:
|
||||
domain: counter
|
||||
maximum_reached: *trigger_common
|
||||
minimum_reached: *trigger_common
|
||||
reset: *trigger_common
|
||||
@@ -1,7 +1,5 @@
|
||||
"""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
|
||||
@@ -10,11 +8,9 @@ from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
|
||||
from .models import CoverDomainSpec
|
||||
|
||||
|
||||
class CoverConditionBase(EntityConditionBase):
|
||||
class CoverConditionBase(EntityConditionBase[CoverDomainSpec]):
|
||||
"""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,13 +1,16 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"trigger_behavior_name": "Trigger when"
|
||||
"condition_behavior_description": "How the state should match on the targeted covers.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted covers to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"awning_is_closed": {
|
||||
"description": "Tests if one or more awnings are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -17,6 +20,7 @@
|
||||
"description": "Tests if one or more awnings are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -26,6 +30,7 @@
|
||||
"description": "Tests if one or more blinds are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -35,6 +40,7 @@
|
||||
"description": "Tests if one or more blinds are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -44,6 +50,7 @@
|
||||
"description": "Tests if one or more curtains are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -53,6 +60,7 @@
|
||||
"description": "Tests if one or more curtains are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -62,6 +70,7 @@
|
||||
"description": "Tests if one or more shades are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -71,6 +80,7 @@
|
||||
"description": "Tests if one or more shades are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -80,6 +90,7 @@
|
||||
"description": "Tests if one or more shutters are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -89,6 +100,7 @@
|
||||
"description": "Tests if one or more shutters are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -196,19 +208,19 @@
|
||||
"services": {
|
||||
"close_cover": {
|
||||
"description": "Closes a cover.",
|
||||
"name": "Close cover"
|
||||
"name": "[%key:common::action::close%]"
|
||||
},
|
||||
"close_cover_tilt": {
|
||||
"description": "Tilts a cover to close.",
|
||||
"name": "Close cover tilt"
|
||||
"name": "Close tilt"
|
||||
},
|
||||
"open_cover": {
|
||||
"description": "Opens a cover.",
|
||||
"name": "Open cover"
|
||||
"name": "[%key:common::action::open%]"
|
||||
},
|
||||
"open_cover_tilt": {
|
||||
"description": "Tilts a cover open.",
|
||||
"name": "Open cover tilt"
|
||||
"name": "Open tilt"
|
||||
},
|
||||
"set_cover_position": {
|
||||
"description": "Moves a cover to a specific position.",
|
||||
@@ -218,7 +230,7 @@
|
||||
"name": "Position"
|
||||
}
|
||||
},
|
||||
"name": "Set cover position"
|
||||
"name": "Set position"
|
||||
},
|
||||
"set_cover_tilt_position": {
|
||||
"description": "Moves a cover tilt to a specific position.",
|
||||
@@ -228,23 +240,23 @@
|
||||
"name": "Tilt position"
|
||||
}
|
||||
},
|
||||
"name": "Set cover tilt position"
|
||||
"name": "Set tilt position"
|
||||
},
|
||||
"stop_cover": {
|
||||
"description": "Stops a cover's movement.",
|
||||
"name": "Stop cover"
|
||||
"description": "Stops the cover movement.",
|
||||
"name": "[%key:common::action::stop%]"
|
||||
},
|
||||
"stop_cover_tilt": {
|
||||
"description": "Stops a tilting cover movement.",
|
||||
"name": "Stop cover tilt"
|
||||
"name": "Stop tilt"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Toggles a cover open/closed.",
|
||||
"name": "Toggle cover"
|
||||
"name": "[%key:common::action::toggle%]"
|
||||
},
|
||||
"toggle_cover_tilt": {
|
||||
"description": "Toggles a cover tilt open/closed.",
|
||||
"name": "Toggle cover tilt"
|
||||
"name": "Toggle tilt"
|
||||
}
|
||||
},
|
||||
"title": "Cover",
|
||||
@@ -253,6 +265,7 @@
|
||||
"description": "Triggers after one or more awnings close.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -262,6 +275,7 @@
|
||||
"description": "Triggers after one or more awnings open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -271,6 +285,7 @@
|
||||
"description": "Triggers after one or more blinds close.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -280,6 +295,7 @@
|
||||
"description": "Triggers after one or more blinds open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -289,6 +305,7 @@
|
||||
"description": "Triggers after one or more curtains close.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -298,6 +315,7 @@
|
||||
"description": "Triggers after one or more curtains open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -307,6 +325,7 @@
|
||||
"description": "Triggers after one or more shades close.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -316,6 +335,7 @@
|
||||
"description": "Triggers after one or more shades open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -325,6 +345,7 @@
|
||||
"description": "Triggers after one or more shutters close.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -334,6 +355,7 @@
|
||||
"description": "Triggers after one or more shutters open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""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
|
||||
@@ -10,11 +8,9 @@ from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
|
||||
from .models import CoverDomainSpec
|
||||
|
||||
|
||||
class CoverTriggerBase(EntityTriggerBase):
|
||||
class CoverTriggerBase(EntityTriggerBase[CoverDomainSpec]):
|
||||
"""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]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"services": {
|
||||
"set_value": {
|
||||
"description": "Sets the value of a date.",
|
||||
"description": "Sets the date.",
|
||||
"fields": {
|
||||
"date": {
|
||||
"description": "The date to set.",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"services": {
|
||||
"set_value": {
|
||||
"description": "Sets the value of a date/time.",
|
||||
"description": "Sets the date/time for a datetime entity.",
|
||||
"fields": {
|
||||
"datetime": {
|
||||
"description": "The date/time to set. The time zone of the Home Assistant instance is assumed.",
|
||||
|
||||
@@ -1,91 +1 @@
|
||||
"""The Leviton Decora Wi-Fi integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
|
||||
from decora_wifi import DecoraWiFiSession
|
||||
from decora_wifi.models.iot_switch import IotSwitch
|
||||
from decora_wifi.models.person import Person
|
||||
from decora_wifi.models.residence import Residence
|
||||
from decora_wifi.models.residential_account import ResidentialAccount
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
PLATFORMS = [Platform.LIGHT]
|
||||
|
||||
type DecoraWifiConfigEntry = ConfigEntry[DecoraWifiData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DecoraWifiData:
|
||||
"""Runtime data for the Decora Wi-Fi integration."""
|
||||
|
||||
session: DecoraWiFiSession
|
||||
switches: list[IotSwitch]
|
||||
|
||||
|
||||
def _login_and_get_switches(email: str, password: str) -> DecoraWifiData:
|
||||
"""Log in and fetch all IoT switches. Runs in executor."""
|
||||
session = DecoraWiFiSession()
|
||||
success = session.login(email, password)
|
||||
|
||||
if success is None:
|
||||
raise ConfigEntryAuthFailed("Invalid credentials for myLeviton account")
|
||||
|
||||
perms = session.user.get_residential_permissions()
|
||||
all_switches: list[IotSwitch] = []
|
||||
for permission in perms:
|
||||
if permission.residentialAccountId is not None:
|
||||
acct = ResidentialAccount(session, permission.residentialAccountId)
|
||||
all_switches.extend(
|
||||
switch
|
||||
for residence in acct.get_residences()
|
||||
for switch in residence.get_iot_switches()
|
||||
)
|
||||
elif permission.residenceId is not None:
|
||||
residence = Residence(session, permission.residenceId)
|
||||
all_switches.extend(residence.get_iot_switches())
|
||||
|
||||
return DecoraWifiData(session, all_switches)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: DecoraWifiConfigEntry) -> bool:
|
||||
"""Set up Leviton Decora Wi-Fi from a config entry."""
|
||||
try:
|
||||
data = await hass.async_add_executor_job(
|
||||
_login_and_get_switches,
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
except ValueError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
"Failed to communicate with myLeviton service"
|
||||
) from err
|
||||
|
||||
entry.runtime_data = data
|
||||
|
||||
async def _logout(_: Event | None = None) -> None:
|
||||
with suppress(ValueError):
|
||||
await hass.async_add_executor_job(Person.logout, data.session)
|
||||
|
||||
entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _logout))
|
||||
entry.async_on_unload(_logout)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DecoraWifiConfigEntry) -> bool:
|
||||
"""Unload a Decora Wi-Fi config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
"""The decora_wifi component."""
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
"""Config flow for Leviton Decora Wi-Fi integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from typing import Any
|
||||
|
||||
from decora_wifi import DecoraWiFiSession
|
||||
from decora_wifi.models.person import Person
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): TextSelector(),
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _try_login(email: str, password: str) -> str | None:
|
||||
"""Attempt to log in, return the user ID, or None on auth failure."""
|
||||
session = DecoraWiFiSession()
|
||||
if session.login(email, password) is None:
|
||||
return None
|
||||
user_id = str(session.user._id) # noqa: SLF001
|
||||
with contextlib.suppress(ValueError):
|
||||
Person.logout(session)
|
||||
return user_id
|
||||
|
||||
|
||||
class DecoraWifiConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Leviton Decora Wi-Fi config flow."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
user_id = await self.hass.async_add_executor_job(
|
||||
_try_login,
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
except ValueError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
if user_id is None:
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
await self.async_set_unique_id(user_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME],
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(USER_SCHEMA, user_input),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle import from YAML configuration."""
|
||||
self._async_abort_entries_match({CONF_USERNAME: import_data[CONF_USERNAME]})
|
||||
|
||||
try:
|
||||
user_id = await self.hass.async_add_executor_job(
|
||||
_try_login,
|
||||
import_data[CONF_USERNAME],
|
||||
import_data[CONF_PASSWORD],
|
||||
)
|
||||
except ValueError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
if user_id is None:
|
||||
return self.async_abort(reason="invalid_auth")
|
||||
|
||||
await self.async_set_unique_id(user_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=import_data[CONF_USERNAME],
|
||||
data=import_data,
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user