mirror of
https://github.com/home-assistant/core.git
synced 2026-03-25 17:10:32 +01:00
Compare commits
2 Commits
water_heat
...
retro-east
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e22c8db0fb | ||
|
|
b3e91ded84 |
6
.github/copilot-instructions.md
vendored
6
.github/copilot-instructions.md
vendored
@@ -1,12 +1,6 @@
|
||||
<!-- Automatically generated by gen_copilot_instructions.py, do not edit -->
|
||||
|
||||
|
||||
|
||||
# Copilot code review instructions
|
||||
|
||||
- Start review comments with a short, one-sentence summary of the suggested fix.
|
||||
- Do not add comments about code style, formatting or linting issues.
|
||||
|
||||
# GitHub Copilot & Claude Code Instructions
|
||||
|
||||
This repository contains the core of Home Assistant, a Python 3 based home automation application.
|
||||
|
||||
5
.github/workflows/builder.yml
vendored
5
.github/workflows/builder.yml
vendored
@@ -224,6 +224,7 @@ jobs:
|
||||
matrix:
|
||||
machine:
|
||||
- generic-x86-64
|
||||
- intel-nuc
|
||||
- khadas-vim3
|
||||
- odroid-c2
|
||||
- odroid-c4
|
||||
@@ -247,6 +248,10 @@ jobs:
|
||||
- machine: qemux86-64
|
||||
arch: amd64
|
||||
runs-on: ubuntu-24.04
|
||||
# TODO: remove, intel-nuc is a legacy name for x86-64, renamed in 2021
|
||||
- machine: intel-nuc
|
||||
arch: amd64
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -120,7 +120,7 @@ jobs:
|
||||
run: |
|
||||
echo "key=$(lsb_release -rs)-apt-${CACHE_VERSION}-${HA_SHORT_VERSION}" >> $GITHUB_OUTPUT
|
||||
- name: Filter for core changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: core
|
||||
with:
|
||||
filters: .core_files.yaml
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
echo "Result:"
|
||||
cat .integration_paths.yaml
|
||||
- name: Filter for integration changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: integrations
|
||||
with:
|
||||
filters: .integration_paths.yaml
|
||||
|
||||
@@ -274,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.*
|
||||
@@ -329,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.*
|
||||
|
||||
18
CODEOWNERS
generated
18
CODEOWNERS
generated
@@ -214,8 +214,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/balboa/ @garbled1 @natekspencer
|
||||
/homeassistant/components/bang_olufsen/ @mj23000
|
||||
/tests/components/bang_olufsen/ @mj23000
|
||||
/homeassistant/components/battery/ @home-assistant/core
|
||||
/tests/components/battery/ @home-assistant/core
|
||||
/homeassistant/components/bayesian/ @HarvsG
|
||||
/tests/components/bayesian/ @HarvsG
|
||||
/homeassistant/components/beewi_smartclim/ @alemuro
|
||||
@@ -739,8 +737,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
|
||||
@@ -788,8 +784,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/igloohome/ @keithle888
|
||||
/homeassistant/components/ign_sismologia/ @exxamalte
|
||||
/tests/components/ign_sismologia/ @exxamalte
|
||||
/homeassistant/components/illuminance/ @home-assistant/core
|
||||
/tests/components/illuminance/ @home-assistant/core
|
||||
/homeassistant/components/image/ @home-assistant/core
|
||||
/tests/components/image/ @home-assistant/core
|
||||
/homeassistant/components/image_processing/ @home-assistant/core
|
||||
@@ -949,8 +943,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
|
||||
@@ -1079,8 +1071,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
|
||||
@@ -1319,8 +1309,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/poolsense/ @haemishkyd
|
||||
/homeassistant/components/portainer/ @erwindouna
|
||||
/tests/components/portainer/ @erwindouna
|
||||
/homeassistant/components/power/ @home-assistant/core
|
||||
/tests/components/power/ @home-assistant/core
|
||||
/homeassistant/components/powerfox/ @klaasnicolaas
|
||||
/tests/components/powerfox/ @klaasnicolaas
|
||||
/homeassistant/components/powerfox_local/ @klaasnicolaas
|
||||
@@ -1606,8 +1594,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
||||
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
|
||||
/tests/components/solarlog/ @Ernst79 @dontinelli
|
||||
/homeassistant/components/solarman/ @solarmanpv
|
||||
/tests/components/solarman/ @solarmanpv
|
||||
/homeassistant/components/solax/ @squishykid @Darsstar
|
||||
/tests/components/solax/ @squishykid @Darsstar
|
||||
/homeassistant/components/soma/ @ratsept
|
||||
@@ -1717,8 +1703,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tellduslive/ @fredrike
|
||||
/homeassistant/components/teltonika/ @karlbeecken
|
||||
/tests/components/teltonika/ @karlbeecken
|
||||
/homeassistant/components/temperature/ @home-assistant/core
|
||||
/tests/components/temperature/ @home-assistant/core
|
||||
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
||||
/tests/components/template/ @Petro31 @home-assistant/core
|
||||
/homeassistant/components/tesla_fleet/ @Bre77
|
||||
@@ -1764,8 +1748,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
|
||||
|
||||
|
||||
@@ -241,18 +241,12 @@ DEFAULT_INTEGRATIONS = {
|
||||
*BASE_PLATFORMS,
|
||||
#
|
||||
# Integrations providing triggers and conditions for base platforms:
|
||||
"air_quality",
|
||||
"battery",
|
||||
"door",
|
||||
"garage_door",
|
||||
"gate",
|
||||
"humidity",
|
||||
"illuminance",
|
||||
"moisture",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"power",
|
||||
"temperature",
|
||||
"window",
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,150 +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, NumericalDomainSpec
|
||||
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: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"is_ozone_value": make_entity_numerical_condition_with_unit(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
OzoneConcentrationConverter,
|
||||
),
|
||||
"is_voc_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
MassVolumeConcentrationConverter,
|
||||
),
|
||||
"is_voc_ratio_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
},
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
UnitlessRatioConverter,
|
||||
),
|
||||
"is_no_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
),
|
||||
"is_no2_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
),
|
||||
"is_so2_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
),
|
||||
# Numerical sensor conditions without unit conversion (single-unit device classes)
|
||||
"is_co2_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"is_pm1_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm25_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm4_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm10_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_n2o_value": make_entity_numerical_condition(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
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,588 +0,0 @@
|
||||
# --- Common condition fields ---
|
||||
|
||||
.condition_behavior: &condition_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
# --- Number or entity selectors ---
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
# --- Unit selectors ---
|
||||
|
||||
.unit_co: &unit_co
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "mg/m³"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_ozone: &unit_ozone
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_no2: &unit_no2
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_no: &unit_no
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_so2: &unit_so2
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_voc: &unit_voc
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "μg/m³"
|
||||
- "mg/m³"
|
||||
|
||||
.unit_voc_ratio: &unit_voc_ratio
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
|
||||
# --- 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
|
||||
above: *number_or_entity_co
|
||||
below: *number_or_entity_co
|
||||
unit: *unit_co
|
||||
|
||||
is_ozone_value:
|
||||
target: *target_ozone
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_ozone
|
||||
below: *number_or_entity_ozone
|
||||
unit: *unit_ozone
|
||||
|
||||
is_voc_value:
|
||||
target: *target_voc
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_voc
|
||||
below: *number_or_entity_voc
|
||||
unit: *unit_voc
|
||||
|
||||
is_voc_ratio_value:
|
||||
target: *target_voc_ratio
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_voc_ratio
|
||||
below: *number_or_entity_voc_ratio
|
||||
unit: *unit_voc_ratio
|
||||
|
||||
is_no_value:
|
||||
target: *target_no
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_no
|
||||
below: *number_or_entity_no
|
||||
unit: *unit_no
|
||||
|
||||
is_no2_value:
|
||||
target: *target_no2
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_no2
|
||||
below: *number_or_entity_no2
|
||||
unit: *unit_no2
|
||||
|
||||
is_so2_value:
|
||||
target: *target_so2
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_so2
|
||||
below: *number_or_entity_so2
|
||||
unit: *unit_so2
|
||||
|
||||
# --- Numerical sensor conditions without unit conversion ---
|
||||
|
||||
is_co2_value:
|
||||
target: *target_co2
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_co2
|
||||
below: *number_or_entity_co2
|
||||
|
||||
is_pm1_value:
|
||||
target: *target_pm1
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_pm1
|
||||
below: *number_or_entity_pm1
|
||||
|
||||
is_pm25_value:
|
||||
target: *target_pm25
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_pm25
|
||||
below: *number_or_entity_pm25
|
||||
|
||||
is_pm4_value:
|
||||
target: *target_pm4
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_pm4
|
||||
below: *number_or_entity_pm4
|
||||
|
||||
is_pm10_value:
|
||||
target: *target_pm10
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_pm10
|
||||
below: *number_or_entity_pm10
|
||||
|
||||
is_n2o_value:
|
||||
target: *target_n2o
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_n2o
|
||||
below: *number_or_entity_n2o
|
||||
@@ -1,164 +1,7 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"co2_changed": {
|
||||
"trigger": "mdi:molecule-co2"
|
||||
},
|
||||
"co2_crossed_threshold": {
|
||||
"trigger": "mdi:molecule-co2"
|
||||
},
|
||||
"co_changed": {
|
||||
"trigger": "mdi:molecule-co"
|
||||
},
|
||||
"co_cleared": {
|
||||
"trigger": "mdi:check-circle"
|
||||
},
|
||||
"co_crossed_threshold": {
|
||||
"trigger": "mdi:molecule-co"
|
||||
},
|
||||
"co_detected": {
|
||||
"trigger": "mdi:molecule-co"
|
||||
},
|
||||
"gas_cleared": {
|
||||
"trigger": "mdi:check-circle"
|
||||
},
|
||||
"gas_detected": {
|
||||
"trigger": "mdi:gas-cylinder"
|
||||
},
|
||||
"n2o_changed": {
|
||||
"trigger": "mdi:factory"
|
||||
},
|
||||
"n2o_crossed_threshold": {
|
||||
"trigger": "mdi:factory"
|
||||
},
|
||||
"no2_changed": {
|
||||
"trigger": "mdi:factory"
|
||||
},
|
||||
"no2_crossed_threshold": {
|
||||
"trigger": "mdi:factory"
|
||||
},
|
||||
"no_changed": {
|
||||
"trigger": "mdi:factory"
|
||||
},
|
||||
"no_crossed_threshold": {
|
||||
"trigger": "mdi:factory"
|
||||
},
|
||||
"ozone_changed": {
|
||||
"trigger": "mdi:weather-sunny-alert"
|
||||
},
|
||||
"ozone_crossed_threshold": {
|
||||
"trigger": "mdi:weather-sunny-alert"
|
||||
},
|
||||
"pm10_changed": {
|
||||
"trigger": "mdi:blur"
|
||||
},
|
||||
"pm10_crossed_threshold": {
|
||||
"trigger": "mdi:blur"
|
||||
},
|
||||
"pm1_changed": {
|
||||
"trigger": "mdi:blur"
|
||||
},
|
||||
"pm1_crossed_threshold": {
|
||||
"trigger": "mdi:blur"
|
||||
},
|
||||
"pm25_changed": {
|
||||
"trigger": "mdi:blur"
|
||||
},
|
||||
"pm25_crossed_threshold": {
|
||||
"trigger": "mdi:blur"
|
||||
},
|
||||
"pm4_changed": {
|
||||
"trigger": "mdi:blur"
|
||||
},
|
||||
"pm4_crossed_threshold": {
|
||||
"trigger": "mdi:blur"
|
||||
},
|
||||
"smoke_cleared": {
|
||||
"trigger": "mdi:check-circle"
|
||||
},
|
||||
"smoke_detected": {
|
||||
"trigger": "mdi:smoke-detector-variant"
|
||||
},
|
||||
"so2_changed": {
|
||||
"trigger": "mdi:factory"
|
||||
},
|
||||
"so2_crossed_threshold": {
|
||||
"trigger": "mdi:factory"
|
||||
},
|
||||
"voc_changed": {
|
||||
"trigger": "mdi:air-filter"
|
||||
},
|
||||
"voc_crossed_threshold": {
|
||||
"trigger": "mdi:air-filter"
|
||||
},
|
||||
"voc_ratio_changed": {
|
||||
"trigger": "mdi:air-filter"
|
||||
},
|
||||
"voc_ratio_crossed_threshold": {
|
||||
"trigger": "mdi:air-filter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,964 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_above_description": "Require the value to be above this value.",
|
||||
"condition_above_name": "Above",
|
||||
"condition_behavior_description": "How the value should match on the targeted entities.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"condition_below_description": "Require the value to be below this value.",
|
||||
"condition_below_name": "Below",
|
||||
"condition_unit_description": "All values will be converted to this unit when evaluating the condition.",
|
||||
"condition_unit_name": "Unit of measurement",
|
||||
"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"
|
||||
},
|
||||
"conditions": {
|
||||
"is_co2_value": {
|
||||
"description": "Tests the carbon dioxide level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon dioxide value"
|
||||
},
|
||||
"is_co_cleared": {
|
||||
"description": "Tests if one or more carbon monoxide sensors are cleared.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"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": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"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": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide value"
|
||||
},
|
||||
"is_gas_cleared": {
|
||||
"description": "Tests if one or more gas sensors are cleared.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"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": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"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": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrous oxide value"
|
||||
},
|
||||
"is_no2_value": {
|
||||
"description": "Tests the nitrogen dioxide level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen dioxide value"
|
||||
},
|
||||
"is_no_value": {
|
||||
"description": "Tests the nitrogen monoxide level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen monoxide value"
|
||||
},
|
||||
"is_ozone_value": {
|
||||
"description": "Tests the ozone level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Ozone value"
|
||||
},
|
||||
"is_pm10_value": {
|
||||
"description": "Tests the PM10 level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM10 value"
|
||||
},
|
||||
"is_pm1_value": {
|
||||
"description": "Tests the PM1 level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM1 value"
|
||||
},
|
||||
"is_pm25_value": {
|
||||
"description": "Tests the PM2.5 level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM2.5 value"
|
||||
},
|
||||
"is_pm4_value": {
|
||||
"description": "Tests the PM4 level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM4 value"
|
||||
},
|
||||
"is_smoke_cleared": {
|
||||
"description": "Tests if one or more smoke sensors are cleared.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"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": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"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": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Sulphur dioxide value"
|
||||
},
|
||||
"is_voc_ratio_value": {
|
||||
"description": "Tests the volatile organic compounds ratio of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds ratio value"
|
||||
},
|
||||
"is_voc_value": {
|
||||
"description": "Tests the volatile organic compounds level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds value"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
"number": "Number"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
},
|
||||
"trigger_threshold_type": {
|
||||
"options": {
|
||||
"above": "Above",
|
||||
"below": "Below",
|
||||
"between": "Between",
|
||||
"outside": "Outside"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Air Quality",
|
||||
"triggers": {
|
||||
"co2_changed": {
|
||||
"description": "Triggers after one or more carbon dioxide levels change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"co2_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"co_changed": {
|
||||
"description": "Triggers after one or more carbon monoxide levels change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"co_cleared": {
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide cleared"
|
||||
},
|
||||
"co_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"co_detected": {
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide detected"
|
||||
},
|
||||
"gas_cleared": {
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas cleared"
|
||||
},
|
||||
"gas_detected": {
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas detected"
|
||||
},
|
||||
"n2o_changed": {
|
||||
"description": "Triggers after one or more nitrous oxide levels change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"n2o_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"no2_changed": {
|
||||
"description": "Triggers after one or more nitrogen dioxide levels change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"no2_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"no_changed": {
|
||||
"description": "Triggers after one or more nitrogen monoxide levels change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"no_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"ozone_changed": {
|
||||
"description": "Triggers after one or more ozone levels change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"ozone_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"pm10_changed": {
|
||||
"description": "Triggers after one or more PM10 levels change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"pm10_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"pm1_changed": {
|
||||
"description": "Triggers after one or more PM1 levels change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"pm1_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"pm25_changed": {
|
||||
"description": "Triggers after one or more PM2.5 levels change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"pm25_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"pm4_changed": {
|
||||
"description": "Triggers after one or more PM4 levels change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"pm4_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"smoke_cleared": {
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke cleared"
|
||||
},
|
||||
"smoke_detected": {
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke detected"
|
||||
},
|
||||
"so2_changed": {
|
||||
"description": "Triggers after one or more sulphur dioxide levels change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"so2_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"voc_changed": {
|
||||
"description": "Triggers after one or more volatile organic compound levels change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"voc_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"voc_ratio_changed": {
|
||||
"description": "Triggers after one or more volatile organic compound ratios change.",
|
||||
"fields": {
|
||||
"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"
|
||||
},
|
||||
"voc_ratio_crossed_threshold": {
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
"""Provides triggers 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, NumericalDomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
EntityTargetStateTriggerBase,
|
||||
Trigger,
|
||||
make_entity_numerical_state_changed_trigger,
|
||||
make_entity_numerical_state_changed_with_unit_trigger,
|
||||
make_entity_numerical_state_crossed_threshold_trigger,
|
||||
make_entity_numerical_state_crossed_threshold_with_unit_trigger,
|
||||
make_entity_target_state_trigger,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import (
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
MassVolumeConcentrationConverter,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
OzoneConcentrationConverter,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
UnitlessRatioConverter,
|
||||
)
|
||||
|
||||
|
||||
def _make_detected_trigger(
|
||||
device_class: BinarySensorDeviceClass,
|
||||
) -> type[EntityTargetStateTriggerBase]:
|
||||
"""Create a detected trigger for a binary sensor device class."""
|
||||
|
||||
return make_entity_target_state_trigger(
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
|
||||
)
|
||||
|
||||
|
||||
def _make_cleared_trigger(
|
||||
device_class: BinarySensorDeviceClass,
|
||||
) -> type[EntityTargetStateTriggerBase]:
|
||||
"""Create a cleared trigger for a binary sensor device class."""
|
||||
|
||||
return make_entity_target_state_trigger(
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
|
||||
)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
# Binary sensor triggers (detected/cleared)
|
||||
"gas_detected": _make_detected_trigger(BinarySensorDeviceClass.GAS),
|
||||
"gas_cleared": _make_cleared_trigger(BinarySensorDeviceClass.GAS),
|
||||
"co_detected": _make_detected_trigger(BinarySensorDeviceClass.CO),
|
||||
"co_cleared": _make_cleared_trigger(BinarySensorDeviceClass.CO),
|
||||
"smoke_detected": _make_detected_trigger(BinarySensorDeviceClass.SMOKE),
|
||||
"smoke_cleared": _make_cleared_trigger(BinarySensorDeviceClass.SMOKE),
|
||||
# Numerical sensor triggers with unit conversion
|
||||
"co_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"co_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"ozone_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{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: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
OzoneConcentrationConverter,
|
||||
),
|
||||
"voc_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
MassVolumeConcentrationConverter,
|
||||
),
|
||||
"voc_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
MassVolumeConcentrationConverter,
|
||||
),
|
||||
"voc_ratio_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
},
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
UnitlessRatioConverter,
|
||||
),
|
||||
"voc_ratio_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
},
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
UnitlessRatioConverter,
|
||||
),
|
||||
"no_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
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: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
),
|
||||
"no2_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
),
|
||||
"no2_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
),
|
||||
"so2_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
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: 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: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"co2_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"pm1_changed": make_entity_numerical_state_changed_trigger(
|
||||
{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: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm25_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm25_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm4_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm4_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm10_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm10_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"n2o_changed": make_entity_numerical_state_changed_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROUS_OXIDE
|
||||
)
|
||||
},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"n2o_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROUS_OXIDE
|
||||
)
|
||||
},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for air quality."""
|
||||
return TRIGGERS
|
||||
@@ -1,692 +0,0 @@
|
||||
.trigger_common_fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.unit_co: &unit_co
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "mg/m³"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_ozone: &unit_ozone
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_no2: &unit_no2
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_no: &unit_no
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_so2: &unit_so2
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_voc: &unit_voc
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "μg/m³"
|
||||
- "mg/m³"
|
||||
|
||||
.unit_voc_ratio: &unit_voc_ratio
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
|
||||
.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
|
||||
behavior: *trigger_behavior
|
||||
|
||||
# --- 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 triggers ---
|
||||
|
||||
gas_detected:
|
||||
fields: *trigger_binary_fields
|
||||
target: *target_gas
|
||||
|
||||
gas_cleared:
|
||||
fields: *trigger_binary_fields
|
||||
target: *target_gas
|
||||
|
||||
co_detected:
|
||||
fields: *trigger_binary_fields
|
||||
target: *target_co_binary
|
||||
|
||||
co_cleared:
|
||||
fields: *trigger_binary_fields
|
||||
target: *target_co_binary
|
||||
|
||||
smoke_detected:
|
||||
fields: *trigger_binary_fields
|
||||
target: *target_smoke
|
||||
|
||||
smoke_cleared:
|
||||
fields: *trigger_binary_fields
|
||||
target: *target_smoke
|
||||
|
||||
# --- Numerical sensor triggers ---
|
||||
|
||||
co_changed:
|
||||
target: *target_co_sensor
|
||||
fields:
|
||||
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_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_co
|
||||
upper_limit: *number_or_entity_co
|
||||
unit: *unit_co
|
||||
|
||||
co2_changed:
|
||||
target: *target_co2
|
||||
fields:
|
||||
above: *number_or_entity_co2
|
||||
below: *number_or_entity_co2
|
||||
|
||||
co2_crossed_threshold:
|
||||
target: *target_co2
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_co2
|
||||
upper_limit: *number_or_entity_co2
|
||||
|
||||
pm1_changed:
|
||||
target: *target_pm1
|
||||
fields:
|
||||
above: *number_or_entity_pm1
|
||||
below: *number_or_entity_pm1
|
||||
|
||||
pm1_crossed_threshold:
|
||||
target: *target_pm1
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_pm1
|
||||
upper_limit: *number_or_entity_pm1
|
||||
|
||||
pm25_changed:
|
||||
target: *target_pm25
|
||||
fields:
|
||||
above: *number_or_entity_pm25
|
||||
below: *number_or_entity_pm25
|
||||
|
||||
pm25_crossed_threshold:
|
||||
target: *target_pm25
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_pm25
|
||||
upper_limit: *number_or_entity_pm25
|
||||
|
||||
pm4_changed:
|
||||
target: *target_pm4
|
||||
fields:
|
||||
above: *number_or_entity_pm4
|
||||
below: *number_or_entity_pm4
|
||||
|
||||
pm4_crossed_threshold:
|
||||
target: *target_pm4
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_pm4
|
||||
upper_limit: *number_or_entity_pm4
|
||||
|
||||
pm10_changed:
|
||||
target: *target_pm10
|
||||
fields:
|
||||
above: *number_or_entity_pm10
|
||||
below: *number_or_entity_pm10
|
||||
|
||||
pm10_crossed_threshold:
|
||||
target: *target_pm10
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_pm10
|
||||
upper_limit: *number_or_entity_pm10
|
||||
|
||||
ozone_changed:
|
||||
target: *target_ozone
|
||||
fields:
|
||||
above: *number_or_entity_ozone
|
||||
below: *number_or_entity_ozone
|
||||
unit: *unit_ozone
|
||||
|
||||
ozone_crossed_threshold:
|
||||
target: *target_ozone
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_ozone
|
||||
upper_limit: *number_or_entity_ozone
|
||||
unit: *unit_ozone
|
||||
|
||||
voc_changed:
|
||||
target: *target_voc
|
||||
fields:
|
||||
above: *number_or_entity_voc
|
||||
below: *number_or_entity_voc
|
||||
unit: *unit_voc
|
||||
|
||||
voc_crossed_threshold:
|
||||
target: *target_voc
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_voc
|
||||
upper_limit: *number_or_entity_voc
|
||||
unit: *unit_voc
|
||||
|
||||
voc_ratio_changed:
|
||||
target: *target_voc_ratio
|
||||
fields:
|
||||
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_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_voc_ratio
|
||||
upper_limit: *number_or_entity_voc_ratio
|
||||
unit: *unit_voc_ratio
|
||||
|
||||
no_changed:
|
||||
target: *target_no
|
||||
fields:
|
||||
above: *number_or_entity_no
|
||||
below: *number_or_entity_no
|
||||
unit: *unit_no
|
||||
|
||||
no_crossed_threshold:
|
||||
target: *target_no
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_no
|
||||
upper_limit: *number_or_entity_no
|
||||
unit: *unit_no
|
||||
|
||||
no2_changed:
|
||||
target: *target_no2
|
||||
fields:
|
||||
above: *number_or_entity_no2
|
||||
below: *number_or_entity_no2
|
||||
unit: *unit_no2
|
||||
|
||||
no2_crossed_threshold:
|
||||
target: *target_no2
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_no2
|
||||
upper_limit: *number_or_entity_no2
|
||||
unit: *unit_no2
|
||||
|
||||
n2o_changed:
|
||||
target: *target_n2o
|
||||
fields:
|
||||
above: *number_or_entity_n2o
|
||||
below: *number_or_entity_n2o
|
||||
|
||||
n2o_crossed_threshold:
|
||||
target: *target_n2o
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_n2o
|
||||
upper_limit: *number_or_entity_n2o
|
||||
|
||||
so2_changed:
|
||||
target: *target_so2
|
||||
fields:
|
||||
above: *number_or_entity_so2
|
||||
below: *number_or_entity_so2
|
||||
unit: *unit_so2
|
||||
|
||||
so2_crossed_threshold:
|
||||
target: *target_so2
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
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"])
|
||||
|
||||
@@ -173,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.",
|
||||
@@ -183,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.",
|
||||
@@ -193,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.",
|
||||
@@ -203,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.",
|
||||
@@ -213,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.",
|
||||
@@ -223,7 +223,7 @@
|
||||
"name": "Code"
|
||||
}
|
||||
},
|
||||
"name": "Disarm alarm"
|
||||
"name": "Disarm"
|
||||
},
|
||||
"alarm_trigger": {
|
||||
"description": "Triggers an alarm manually.",
|
||||
@@ -233,7 +233,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Trigger alarm"
|
||||
"name": "Trigger"
|
||||
}
|
||||
},
|
||||
"title": "Alarm control panel",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==13.3.0"]
|
||||
"requirements": ["aioamazondevices==13.0.1"]
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
@@ -646,8 +662,7 @@ class PipelineRun:
|
||||
"""Emit run start event."""
|
||||
self._device_id = device_id
|
||||
self._satellite_id = satellite_id
|
||||
if self.start_stage in (PipelineStage.WAKE_WORD, PipelineStage.STT):
|
||||
self._start_debug_recording_thread()
|
||||
self._start_debug_recording_thread()
|
||||
|
||||
data: dict[str, Any] = {
|
||||
"pipeline": self.pipeline.id,
|
||||
@@ -1489,7 +1504,9 @@ class PipelineRun:
|
||||
|
||||
def _start_debug_recording_thread(self) -> None:
|
||||
"""Start thread to record wake/stt audio if debug_recording_dir is set."""
|
||||
assert self.debug_recording_thread is None
|
||||
if self.debug_recording_thread is not None:
|
||||
# Already started
|
||||
return
|
||||
|
||||
# Directory to save audio for each pipeline run.
|
||||
# Configured in YAML for assist_pipeline.
|
||||
@@ -1664,39 +1681,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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -115,7 +115,6 @@ def async_setup(
|
||||
) -> None:
|
||||
"""Component to allow users to login."""
|
||||
hass.http.register_view(WellKnownOAuthInfoView)
|
||||
hass.http.register_view(WellKnownProtectedResourceView)
|
||||
hass.http.register_view(AuthProvidersView)
|
||||
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow, store_result))
|
||||
hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result))
|
||||
@@ -155,32 +154,6 @@ class WellKnownOAuthInfoView(HomeAssistantView):
|
||||
return self.json(metadata)
|
||||
|
||||
|
||||
class WellKnownProtectedResourceView(HomeAssistantView):
|
||||
"""View to host the OAuth2 Protected Resource Metadata per RFC9728."""
|
||||
|
||||
requires_auth = False
|
||||
url = "/.well-known/oauth-protected-resource"
|
||||
name = "well-known/oauth-protected-resource"
|
||||
|
||||
async def get(self, request: web.Request) -> web.Response:
|
||||
"""Return the protected resource metadata."""
|
||||
hass = request.app[KEY_HASS]
|
||||
try:
|
||||
url_prefix = get_url(hass, require_current_request=True)
|
||||
except NoURLAvailableError:
|
||||
return self.json_message("No URL available", HTTPStatus.NOT_FOUND)
|
||||
|
||||
return self.json(
|
||||
{
|
||||
"resource": url_prefix,
|
||||
"authorization_servers": [url_prefix],
|
||||
"resource_documentation": (
|
||||
"https://developers.home-assistant.io/docs/auth_api"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AuthProvidersView(HomeAssistantView):
|
||||
"""View to get available auth providers."""
|
||||
|
||||
|
||||
@@ -118,10 +118,8 @@ SERVICE_TRIGGER = "trigger"
|
||||
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
|
||||
|
||||
_EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"air_quality",
|
||||
"alarm_control_panel",
|
||||
"assist_satellite",
|
||||
"battery",
|
||||
"climate",
|
||||
"cover",
|
||||
"device_tracker",
|
||||
@@ -130,8 +128,6 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"garage_door",
|
||||
"gate",
|
||||
"humidifier",
|
||||
"humidity",
|
||||
"illuminance",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"lock",
|
||||
@@ -139,18 +135,14 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"motion",
|
||||
"occupancy",
|
||||
"person",
|
||||
"power",
|
||||
"schedule",
|
||||
"siren",
|
||||
"switch",
|
||||
"text",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
"window",
|
||||
}
|
||||
|
||||
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"air_quality",
|
||||
"alarm_control_panel",
|
||||
"assist_satellite",
|
||||
"button",
|
||||
@@ -158,33 +150,28 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"door",
|
||||
"event",
|
||||
"fan",
|
||||
"garage_door",
|
||||
"gate",
|
||||
"humidifier",
|
||||
"humidity",
|
||||
"illuminance",
|
||||
"input_boolean",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"moisture",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"person",
|
||||
"power",
|
||||
"remote",
|
||||
"scene",
|
||||
"schedule",
|
||||
"select",
|
||||
"siren",
|
||||
"switch",
|
||||
"temperature",
|
||||
"text",
|
||||
"update",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
"window",
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ class S3BackupAgent(BackupAgent):
|
||||
if backup.size < MULTIPART_MIN_PART_SIZE_BYTES:
|
||||
await self._upload_simple(tar_filename, open_stream)
|
||||
else:
|
||||
await self._upload_multipart(tar_filename, open_stream, on_progress)
|
||||
await self._upload_multipart(tar_filename, open_stream)
|
||||
|
||||
# Upload the metadata file
|
||||
metadata_content = json.dumps(backup.as_dict())
|
||||
@@ -188,13 +188,11 @@ class S3BackupAgent(BackupAgent):
|
||||
self,
|
||||
tar_filename: str,
|
||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||
on_progress: OnProgressCallback,
|
||||
) -> None:
|
||||
):
|
||||
"""Upload a large file using multipart upload.
|
||||
|
||||
:param tar_filename: The target filename for the backup.
|
||||
:param open_stream: A function returning an async iterator that yields bytes.
|
||||
:param on_progress: A callback to report the number of uploaded bytes.
|
||||
"""
|
||||
_LOGGER.debug("Starting multipart upload for %s", tar_filename)
|
||||
multipart_upload = await self._client.create_multipart_upload(
|
||||
@@ -207,7 +205,6 @@ class S3BackupAgent(BackupAgent):
|
||||
part_number = 1
|
||||
buffer = bytearray() # bytes buffer to store the data
|
||||
offset = 0 # start index of unread data inside buffer
|
||||
bytes_uploaded = 0
|
||||
|
||||
stream = await open_stream()
|
||||
async for chunk in stream:
|
||||
@@ -236,8 +233,6 @@ class S3BackupAgent(BackupAgent):
|
||||
Body=part_data.tobytes(),
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||
bytes_uploaded += len(part_data)
|
||||
on_progress(bytes_uploaded=bytes_uploaded)
|
||||
part_number += 1
|
||||
finally:
|
||||
view.release()
|
||||
@@ -266,8 +261,6 @@ class S3BackupAgent(BackupAgent):
|
||||
Body=remaining_data.tobytes(),
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||
bytes_uploaded += len(remaining_data)
|
||||
on_progress(bytes_uploaded=bytes_uploaded)
|
||||
|
||||
await cast(Any, self._client).complete_multipart_upload(
|
||||
Bucket=self._bucket,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
"""Integration for battery conditions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "battery"
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
return True
|
||||
@@ -1,48 +0,0 @@
|
||||
"""Provides conditions for batteries."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
make_entity_numerical_condition,
|
||||
make_entity_state_condition,
|
||||
)
|
||||
|
||||
BATTERY_DOMAIN_SPECS = {
|
||||
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.BATTERY)
|
||||
}
|
||||
BATTERY_CHARGING_DOMAIN_SPECS = {
|
||||
BINARY_SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING
|
||||
)
|
||||
}
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS = {
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
|
||||
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.BATTERY),
|
||||
}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_ON),
|
||||
"is_not_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_OFF),
|
||||
"is_charging": make_entity_state_condition(BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON),
|
||||
"is_not_charging": make_entity_state_condition(
|
||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
|
||||
),
|
||||
"is_level": make_entity_numerical_condition(
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS, PERCENTAGE
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the conditions for batteries."""
|
||||
return CONDITIONS
|
||||
@@ -1,66 +0,0 @@
|
||||
.condition_common: &condition_common
|
||||
target: &target_battery_binary_sensor
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: battery
|
||||
fields:
|
||||
behavior: &condition_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
.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
|
||||
|
||||
is_not_low: *condition_common
|
||||
|
||||
is_charging:
|
||||
target:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: battery_charging
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
|
||||
is_not_charging:
|
||||
target:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: battery_charging
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
|
||||
is_level:
|
||||
target:
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: battery
|
||||
- domain: number
|
||||
device_class: battery
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_charging": {
|
||||
"condition": "mdi:battery-charging"
|
||||
},
|
||||
"is_level": {
|
||||
"condition": "mdi:battery-unknown"
|
||||
},
|
||||
"is_low": {
|
||||
"condition": "mdi:battery-alert"
|
||||
},
|
||||
"is_not_charging": {
|
||||
"condition": "mdi:battery"
|
||||
},
|
||||
"is_not_low": {
|
||||
"condition": "mdi:battery"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"domain": "battery",
|
||||
"name": "Battery",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/battery",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery is charging"
|
||||
},
|
||||
"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%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Require the battery percentage to be below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "Battery level"
|
||||
},
|
||||
"is_low": {
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery is low"
|
||||
},
|
||||
"is_not_charging": {
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery is not charging"
|
||||
},
|
||||
"is_not_low": {
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery is not low"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
"number": "Number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Battery"
|
||||
}
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.5.3",
|
||||
"bluetooth-data-tools==1.28.4",
|
||||
"dbus-fast==3.1.2",
|
||||
"habluetooth==5.11.1"
|
||||
"habluetooth==5.10.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
},
|
||||
"services": {
|
||||
"press": {
|
||||
"description": "Presses a button.",
|
||||
"name": "Press button"
|
||||
"description": "Presses a button entity.",
|
||||
"name": "Press"
|
||||
}
|
||||
},
|
||||
"title": "Button",
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
"name": "Summary"
|
||||
}
|
||||
},
|
||||
"name": "Create calendar event"
|
||||
"name": "Create event"
|
||||
},
|
||||
"get_events": {
|
||||
"description": "Retrieves events on a calendar within a time range.",
|
||||
@@ -108,7 +108,7 @@
|
||||
"name": "Start time"
|
||||
}
|
||||
},
|
||||
"name": "Get calendar events"
|
||||
"name": "Get events"
|
||||
}
|
||||
},
|
||||
"title": "Calendar",
|
||||
|
||||
@@ -432,7 +432,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
)
|
||||
|
||||
# Entity Properties
|
||||
entity_description: CameraEntityDescription
|
||||
_attr_brand: str | None = None
|
||||
_attr_frame_interval: float = MIN_STREAM_INTERVAL
|
||||
_attr_is_on: bool = True
|
||||
|
||||
@@ -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.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,9 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"paused": {
|
||||
"default": "mdi:timer-pause"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,13 +26,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"paused": {
|
||||
"name": "Dimming paused"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"communication_error": {
|
||||
"message": "An error occurred while communicating with the Casper Glow: {error}"
|
||||
|
||||
@@ -1,31 +1,10 @@
|
||||
"""Provides conditions for climates."""
|
||||
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
EntityNumericalConditionWithUnitBase,
|
||||
make_entity_numerical_condition,
|
||||
make_entity_state_condition,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
||||
|
||||
|
||||
class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
|
||||
"""Mixin for climate target temperature conditions with unit conversion."""
|
||||
|
||||
_base_unit = UnitOfTemperature.CELSIUS
|
||||
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_unit_converter = TemperatureConverter
|
||||
|
||||
def _get_entity_unit(self, entity_state: State) -> str | None:
|
||||
"""Get the temperature unit of a climate entity from its state."""
|
||||
# Climate entities convert temperatures to the system unit via show_temp
|
||||
return self._hass.config.units.temperature_unit
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||
|
||||
from .const import ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
|
||||
@@ -49,11 +28,6 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_heating": make_entity_state_condition(
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
|
||||
),
|
||||
"target_humidity": make_entity_numerical_condition(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"target_temperature": ClimateTargetTemperatureCondition,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.condition_common: &condition_common
|
||||
target: &condition_climate_target
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
fields:
|
||||
behavior: &condition_behavior
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
@@ -13,76 +13,8 @@
|
||||
- all
|
||||
- any
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.condition_unit_temperature: &condition_unit_temperature
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "°C"
|
||||
- "°F"
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
is_cooling: *condition_common
|
||||
is_drying: *condition_common
|
||||
is_heating: *condition_common
|
||||
|
||||
target_humidity:
|
||||
target: *condition_climate_target
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_humidity
|
||||
below: *number_or_entity_humidity
|
||||
|
||||
target_temperature:
|
||||
target: *condition_climate_target
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_temperature
|
||||
below: *number_or_entity_temperature
|
||||
unit: *condition_unit_temperature
|
||||
|
||||
@@ -14,12 +14,6 @@
|
||||
},
|
||||
"is_on": {
|
||||
"condition": "mdi:power-on"
|
||||
},
|
||||
"target_humidity": {
|
||||
"condition": "mdi:water-percent"
|
||||
},
|
||||
"target_temperature": {
|
||||
"condition": "mdi:thermometer"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
|
||||
@@ -55,46 +55,6 @@
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device is on"
|
||||
},
|
||||
"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%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Require the target humidity to be below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target humidity"
|
||||
},
|
||||
"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%]"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"device_automation": {
|
||||
@@ -316,67 +276,67 @@
|
||||
},
|
||||
"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.",
|
||||
@@ -395,19 +355,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",
|
||||
@@ -502,10 +462,6 @@
|
||||
"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"
|
||||
@@ -525,10 +481,6 @@
|
||||
"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"
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
|
||||
EntityNumericalStateTriggerWithUnitBase,
|
||||
EntityTargetStateTriggerBase,
|
||||
Trigger,
|
||||
TriggerConfig,
|
||||
@@ -19,7 +16,6 @@ from homeassistant.helpers.trigger import (
|
||||
make_entity_target_state_trigger,
|
||||
make_entity_transition_trigger,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
||||
|
||||
@@ -48,33 +44,6 @@ class HVACModeChangedTrigger(EntityTargetStateTriggerBase):
|
||||
self._to_states = set(self._options[CONF_HVAC_MODE])
|
||||
|
||||
|
||||
class _ClimateTargetTemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitBase):
|
||||
"""Mixin for climate target temperature triggers with unit conversion."""
|
||||
|
||||
_base_unit = UnitOfTemperature.CELSIUS
|
||||
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_unit_converter = TemperatureConverter
|
||||
|
||||
def _get_entity_unit(self, state: State) -> str | None:
|
||||
"""Get the temperature unit of a climate entity from its state."""
|
||||
# Climate entities convert temperatures to the system unit via show_temp
|
||||
return self._hass.config.units.temperature_unit
|
||||
|
||||
|
||||
class ClimateTargetTemperatureChangedTrigger(
|
||||
_ClimateTargetTemperatureTriggerMixin,
|
||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||
):
|
||||
"""Trigger for climate target temperature value changes."""
|
||||
|
||||
|
||||
class ClimateTargetTemperatureCrossedThresholdTrigger(
|
||||
_ClimateTargetTemperatureTriggerMixin,
|
||||
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
|
||||
):
|
||||
"""Trigger for climate target temperature value crossing a threshold."""
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"hvac_mode_changed": HVACModeChangedTrigger,
|
||||
"started_cooling": make_entity_target_state_trigger(
|
||||
@@ -84,15 +53,17 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.DRYING
|
||||
),
|
||||
"target_humidity_changed": make_entity_numerical_state_changed_trigger(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)}
|
||||
),
|
||||
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)}
|
||||
),
|
||||
"target_temperature_changed": make_entity_numerical_state_changed_trigger(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
),
|
||||
"target_temperature_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
),
|
||||
"target_temperature_changed": ClimateTargetTemperatureChangedTrigger,
|
||||
"target_temperature_crossed_threshold": ClimateTargetTemperatureCrossedThresholdTrigger,
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, HVACMode.OFF),
|
||||
"turned_on": make_entity_transition_trigger(
|
||||
DOMAIN,
|
||||
|
||||
@@ -14,29 +14,7 @@
|
||||
- last
|
||||
- any
|
||||
|
||||
.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
|
||||
|
||||
.number_or_entity_temperature: &number_or_entity_temperature
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
@@ -49,24 +27,12 @@
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "°C"
|
||||
- "°F"
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
domain:
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_unit_temperature: &trigger_unit_temperature
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "°C"
|
||||
- "°F"
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
@@ -103,29 +69,27 @@ hvac_mode_changed:
|
||||
target_humidity_changed:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
above: *number_or_entity_humidity
|
||||
below: *number_or_entity_humidity
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
|
||||
target_humidity_crossed_threshold:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_humidity
|
||||
upper_limit: *number_or_entity_humidity
|
||||
lower_limit: *number_or_entity
|
||||
upper_limit: *number_or_entity
|
||||
|
||||
target_temperature_changed:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
above: *number_or_entity_temperature
|
||||
below: *number_or_entity_temperature
|
||||
unit: *trigger_unit_temperature
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
|
||||
target_temperature_crossed_threshold:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_temperature
|
||||
upper_limit: *number_or_entity_temperature
|
||||
unit: *trigger_unit_temperature
|
||||
lower_limit: *number_or_entity
|
||||
upper_limit: *number_or_entity
|
||||
|
||||
@@ -138,7 +138,6 @@ class CloudBackupAgent(BackupAgent):
|
||||
base64md5hash=base64md5hash,
|
||||
metadata=metadata,
|
||||
size=size,
|
||||
on_progress=on_progress,
|
||||
)
|
||||
break
|
||||
except CloudApiNonRetryableError as err:
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==2.2.0", "openai==2.21.0"],
|
||||
"requirements": ["hass-nabucasa==2.0.0", "openai==2.21.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ class R2BackupAgent(BackupAgent):
|
||||
if backup.size < MULTIPART_MIN_PART_SIZE_BYTES:
|
||||
await self._upload_simple(tar_filename, open_stream)
|
||||
else:
|
||||
await self._upload_multipart(tar_filename, open_stream, on_progress)
|
||||
await self._upload_multipart(tar_filename, open_stream)
|
||||
|
||||
# Upload the metadata file
|
||||
metadata_content = json.dumps(backup.as_dict())
|
||||
@@ -185,13 +185,11 @@ class R2BackupAgent(BackupAgent):
|
||||
self,
|
||||
tar_filename: str,
|
||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||
on_progress: OnProgressCallback,
|
||||
) -> None:
|
||||
):
|
||||
"""Upload a large file using multipart upload.
|
||||
|
||||
:param tar_filename: The target filename for the backup.
|
||||
:param open_stream: A function returning an async iterator that yields bytes.
|
||||
:param on_progress: A callback to report the number of uploaded bytes.
|
||||
"""
|
||||
_LOGGER.debug("Starting multipart upload for %s", tar_filename)
|
||||
key = self._with_prefix(tar_filename)
|
||||
@@ -205,7 +203,6 @@ class R2BackupAgent(BackupAgent):
|
||||
part_number = 1
|
||||
buffer = bytearray() # bytes buffer to store the data
|
||||
offset = 0 # start index of unread data inside buffer
|
||||
bytes_uploaded = 0
|
||||
|
||||
stream = await open_stream()
|
||||
async for chunk in stream:
|
||||
@@ -234,8 +231,6 @@ class R2BackupAgent(BackupAgent):
|
||||
Body=part_data.tobytes(),
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||
bytes_uploaded += len(part_data)
|
||||
on_progress(bytes_uploaded=bytes_uploaded)
|
||||
part_number += 1
|
||||
finally:
|
||||
view.release()
|
||||
@@ -264,8 +259,6 @@ class R2BackupAgent(BackupAgent):
|
||||
Body=remaining_data.tobytes(),
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||
bytes_uploaded += len(remaining_data)
|
||||
on_progress(bytes_uploaded=bytes_uploaded)
|
||||
|
||||
await cast(Any, self._client).complete_multipart_upload(
|
||||
Bucket=self._bucket,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.3.24"]
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.3.3"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Provides conditions for covers."""
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.core import HomeAssistant, State, split_entity_id
|
||||
from homeassistant.helpers.condition import Condition, EntityConditionBase
|
||||
|
||||
from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
|
||||
@@ -13,7 +13,7 @@ class CoverConditionBase(EntityConditionBase[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]
|
||||
domain_spec = self._domain_specs[split_entity_id(entity_state.entity_id)[0]]
|
||||
if domain_spec.value_source is not None:
|
||||
return (
|
||||
entity_state.attributes.get(domain_spec.value_source)
|
||||
|
||||
@@ -208,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.",
|
||||
@@ -230,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.",
|
||||
@@ -240,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",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"services": {
|
||||
"reload": {
|
||||
"description": "Reloads derivative sensors from the YAML-configuration.",
|
||||
"name": "Reload derivative sensors"
|
||||
"name": "[%key:common::action::reload%]"
|
||||
}
|
||||
},
|
||||
"title": "Derivative sensor"
|
||||
|
||||
@@ -48,12 +48,7 @@ def async_redact_data[_T](data: _T, to_redact: Iterable[Any]) -> _T:
|
||||
|
||||
|
||||
def _entity_entry_filter(a: attr.Attribute, _: Any) -> bool:
|
||||
return a.name not in (
|
||||
"_cache",
|
||||
"compat_aliases",
|
||||
"compat_name",
|
||||
"original_name_unprefixed",
|
||||
)
|
||||
return a.name not in ("_cache", "compat_aliases", "compat_name")
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -28,7 +28,6 @@ from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigEntry,
|
||||
ConfigEntryState,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
FlowType,
|
||||
@@ -364,11 +363,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# Don't probe to verify the mac is correct since
|
||||
# the host matches (and port matches if provided).
|
||||
raise AbortFlow("already_configured")
|
||||
# If the entry is loaded and the device is currently connected,
|
||||
# don't update the host. This prevents transient mDNS announcements
|
||||
# (e.g., during WiFi mesh roaming) from overwriting a working connection.
|
||||
if entry.state is ConfigEntryState.LOADED and entry.runtime_data.available:
|
||||
raise AbortFlow("already_configured")
|
||||
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
|
||||
await self._fetch_device_info(host, port or configured_port, configured_psk)
|
||||
updates: dict[str, Any] = {}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
"""HTTP view that converts audio from a URL to a preferred format."""
|
||||
|
||||
import asyncio
|
||||
from collections import defaultdict, deque
|
||||
import contextlib
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
import re
|
||||
import secrets
|
||||
from typing import Final
|
||||
|
||||
@@ -24,12 +22,6 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_MAX_CONVERSIONS_PER_DEVICE: Final[int] = 2
|
||||
_MAX_STDERR_LINES: Final[int] = 64
|
||||
_PROC_WAIT_TIMEOUT: Final[int] = 5
|
||||
_STDERR_DRAIN_TIMEOUT: Final[int] = 1
|
||||
_SENSITIVE_QUERY_PARAMS: Final[re.Pattern[str]] = re.compile(
|
||||
r"(?<=[?&])(authSig|token|key|password|secret)=[^&\s]+", re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -223,10 +215,8 @@ class FFmpegConvertResponse(web.StreamResponse):
|
||||
assert proc.stdout is not None
|
||||
assert proc.stderr is not None
|
||||
|
||||
stderr_lines: deque[str] = deque(maxlen=_MAX_STDERR_LINES)
|
||||
stderr_task = self.hass.async_create_background_task(
|
||||
self._collect_ffmpeg_stderr(proc, stderr_lines),
|
||||
"ESPHome media proxy dump stderr",
|
||||
self._dump_ffmpeg_stderr(proc), "ESPHome media proxy dump stderr"
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -245,80 +235,33 @@ class FFmpegConvertResponse(web.StreamResponse):
|
||||
if request.transport:
|
||||
request.transport.abort()
|
||||
raise # don't log error
|
||||
except Exception:
|
||||
except:
|
||||
_LOGGER.exception("Unexpected error during ffmpeg conversion")
|
||||
raise
|
||||
finally:
|
||||
# Allow conversion info to be removed
|
||||
self.convert_info.is_finished = True
|
||||
|
||||
# Ensure subprocess and stderr cleanup run even if this task
|
||||
# is cancelled (e.g., during shutdown)
|
||||
try:
|
||||
# Terminate hangs, so kill is used
|
||||
if proc.returncode is None:
|
||||
proc.kill()
|
||||
# stop dumping ffmpeg stderr task
|
||||
stderr_task.cancel()
|
||||
|
||||
# Wait for process to exit so returncode is set
|
||||
await asyncio.wait_for(proc.wait(), timeout=_PROC_WAIT_TIMEOUT)
|
||||
|
||||
# Let stderr collector finish draining
|
||||
if not stderr_task.done():
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
stderr_task, timeout=_STDERR_DRAIN_TIMEOUT
|
||||
)
|
||||
except TimeoutError:
|
||||
stderr_task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await stderr_task
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"Timed out waiting for ffmpeg process to exit for device %s",
|
||||
self.device_id,
|
||||
)
|
||||
stderr_task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await stderr_task
|
||||
except asyncio.CancelledError:
|
||||
# Kill the process if we were interrupted
|
||||
if proc.returncode is None:
|
||||
proc.kill()
|
||||
stderr_task.cancel()
|
||||
raise
|
||||
|
||||
if proc.returncode is not None and proc.returncode > 0:
|
||||
_LOGGER.error(
|
||||
"FFmpeg conversion failed for device %s (return code %s):\n%s",
|
||||
self.device_id,
|
||||
proc.returncode,
|
||||
"\n".join(
|
||||
_SENSITIVE_QUERY_PARAMS.sub(r"\1=REDACTED", line)
|
||||
for line in stderr_lines
|
||||
),
|
||||
)
|
||||
# Terminate hangs, so kill is used
|
||||
if proc.returncode is None:
|
||||
proc.kill()
|
||||
|
||||
# Close connection by writing EOF unless already closing
|
||||
if request.transport and not request.transport.is_closing():
|
||||
with contextlib.suppress(ConnectionResetError, RuntimeError, OSError):
|
||||
await writer.write_eof()
|
||||
await writer.write_eof()
|
||||
|
||||
async def _collect_ffmpeg_stderr(
|
||||
async def _dump_ffmpeg_stderr(
|
||||
self,
|
||||
proc: asyncio.subprocess.Process,
|
||||
stderr_lines: deque[str],
|
||||
) -> None:
|
||||
"""Collect stderr output from ffmpeg for error reporting."""
|
||||
assert proc.stdout is not None
|
||||
assert proc.stderr is not None
|
||||
|
||||
while self.hass.is_running and (chunk := await proc.stderr.readline()):
|
||||
line = chunk.decode(errors="replace").rstrip()
|
||||
stderr_lines.append(line)
|
||||
_LOGGER.debug(
|
||||
"ffmpeg[%s] output: %s",
|
||||
proc.pid,
|
||||
_SENSITIVE_QUERY_PARAMS.sub(r"\1=REDACTED", line),
|
||||
)
|
||||
_LOGGER.debug("ffmpeg[%s] output: %s", proc.pid, chunk.decode().rstrip())
|
||||
|
||||
|
||||
class FFmpegProxyView(HomeAssistantView):
|
||||
|
||||
@@ -12,10 +12,5 @@
|
||||
"motion": {
|
||||
"default": "mdi:motion-sensor"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"received": {
|
||||
"trigger": "mdi:eye-check"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,17 +21,5 @@
|
||||
"name": "Motion"
|
||||
}
|
||||
},
|
||||
"title": "Event",
|
||||
"triggers": {
|
||||
"received": {
|
||||
"description": "Triggers after one or more event entities receive a matching event.",
|
||||
"fields": {
|
||||
"event_type": {
|
||||
"description": "The event types to trigger on.",
|
||||
"name": "Event type"
|
||||
}
|
||||
},
|
||||
"name": "Event received"
|
||||
}
|
||||
}
|
||||
"title": "Event"
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
"""Provides triggers for events."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_OPTIONS, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
||||
EntityTriggerBase,
|
||||
Trigger,
|
||||
TriggerConfig,
|
||||
)
|
||||
|
||||
from .const import ATTR_EVENT_TYPE, DOMAIN
|
||||
|
||||
CONF_EVENT_TYPE = "event_type"
|
||||
|
||||
EVENT_RECEIVED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_EVENT_TYPE): vol.All(
|
||||
cv.ensure_list, vol.Length(min=1), [cv.string]
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class EventReceivedTrigger(EntityTriggerBase):
|
||||
"""Trigger for event entity when it receives a matching event."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
_schema = EVENT_RECEIVED_TRIGGER_SCHEMA
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the event received trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._event_types = set(self._options[CONF_EVENT_TYPE])
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and different from the current state."""
|
||||
|
||||
# UNKNOWN is a valid from_state, otherwise the first time the event is received
|
||||
# would not trigger
|
||||
if from_state.state == STATE_UNAVAILABLE:
|
||||
return False
|
||||
|
||||
return from_state.state != to_state.state
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the event type is valid and matches one of the configured types."""
|
||||
return (
|
||||
state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
and state.attributes.get(ATTR_EVENT_TYPE) in self._event_types
|
||||
)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"received": EventReceivedTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for events."""
|
||||
return TRIGGERS
|
||||
@@ -1,16 +0,0 @@
|
||||
received:
|
||||
target:
|
||||
entity:
|
||||
domain: event
|
||||
fields:
|
||||
event_type:
|
||||
context:
|
||||
filter_target: target
|
||||
required: true
|
||||
selector:
|
||||
state:
|
||||
attribute: event_type
|
||||
hide_states:
|
||||
- unavailable
|
||||
- unknown
|
||||
multiple: true
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohomeasync", "evohomeasync2"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["evohome-async==1.2.0"]
|
||||
"requirements": ["evohome-async==1.1.3"]
|
||||
}
|
||||
|
||||
@@ -124,17 +124,15 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
||||
until = dt_util.as_utc(until) if until else None
|
||||
|
||||
if operation_mode == STATE_ON:
|
||||
await self.coordinator.call_client_api(
|
||||
self._evo_device.set_on(until=until)
|
||||
)
|
||||
await self.coordinator.call_client_api(self._evo_device.on(until=until))
|
||||
else: # STATE_OFF
|
||||
await self.coordinator.call_client_api(
|
||||
self._evo_device.set_off(until=until)
|
||||
self._evo_device.off(until=until)
|
||||
)
|
||||
|
||||
async def async_turn_away_mode_on(self) -> None:
|
||||
"""Turn away mode on."""
|
||||
await self.coordinator.call_client_api(self._evo_device.set_off())
|
||||
await self.coordinator.call_client_api(self._evo_device.off())
|
||||
|
||||
async def async_turn_away_mode_off(self) -> None:
|
||||
"""Turn away mode off."""
|
||||
@@ -142,8 +140,8 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on."""
|
||||
await self.coordinator.call_client_api(self._evo_device.set_on())
|
||||
await self.coordinator.call_client_api(self._evo_device.on())
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off."""
|
||||
await self.coordinator.call_client_api(self._evo_device.set_off())
|
||||
await self.coordinator.call_client_api(self._evo_device.off())
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
"name": "Decrement"
|
||||
}
|
||||
},
|
||||
"name": "Decrease fan speed"
|
||||
"name": "Decrease speed"
|
||||
},
|
||||
"increase_speed": {
|
||||
"description": "Increases the speed of a fan.",
|
||||
@@ -128,7 +128,7 @@
|
||||
"name": "Increment"
|
||||
}
|
||||
},
|
||||
"name": "Increase fan speed"
|
||||
"name": "Increase speed"
|
||||
},
|
||||
"oscillate": {
|
||||
"description": "Controls the oscillation of a fan.",
|
||||
@@ -138,7 +138,7 @@
|
||||
"name": "Oscillating"
|
||||
}
|
||||
},
|
||||
"name": "Oscillate fan"
|
||||
"name": "Oscillate"
|
||||
},
|
||||
"set_direction": {
|
||||
"description": "Sets a fan's rotation direction.",
|
||||
@@ -148,7 +148,7 @@
|
||||
"name": "Direction"
|
||||
}
|
||||
},
|
||||
"name": "Set fan direction"
|
||||
"name": "Set direction"
|
||||
},
|
||||
"set_percentage": {
|
||||
"description": "Sets the speed of a fan.",
|
||||
@@ -158,28 +158,28 @@
|
||||
"name": "Percentage"
|
||||
}
|
||||
},
|
||||
"name": "Set fan speed"
|
||||
"name": "Set speed"
|
||||
},
|
||||
"set_preset_mode": {
|
||||
"description": "Sets the preset mode of a fan.",
|
||||
"description": "Sets preset fan mode.",
|
||||
"fields": {
|
||||
"preset_mode": {
|
||||
"description": "Preset fan mode.",
|
||||
"name": "Preset mode"
|
||||
}
|
||||
},
|
||||
"name": "Set fan preset mode"
|
||||
"name": "Set preset mode"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Toggles a fan on/off.",
|
||||
"name": "Toggle fan"
|
||||
"name": "[%key:common::action::toggle%]"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Turns off a fan.",
|
||||
"name": "Turn off fan"
|
||||
"description": "Turns fan off.",
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Turns on a fan.",
|
||||
"description": "Turns fan on.",
|
||||
"fields": {
|
||||
"percentage": {
|
||||
"description": "[%key:component::fan::services::set_percentage::fields::percentage::description%]",
|
||||
@@ -190,7 +190,7 @@
|
||||
"name": "[%key:component::fan::services::set_preset_mode::fields::preset_mode::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Turn on fan"
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
"title": "Fan",
|
||||
|
||||
@@ -242,7 +242,7 @@
|
||||
"services": {
|
||||
"reload": {
|
||||
"description": "Reloads filters from the YAML-configuration.",
|
||||
"name": "Reload filters"
|
||||
"name": "[%key:common::action::reload%]"
|
||||
}
|
||||
},
|
||||
"title": "Filter"
|
||||
|
||||
@@ -36,10 +36,10 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=5)
|
||||
class FireflyCoordinatorData:
|
||||
"""Data structure for Firefly III coordinator data."""
|
||||
|
||||
accounts: dict[str, Account]
|
||||
accounts: list[Account]
|
||||
categories: list[Category]
|
||||
category_details: dict[str, Category]
|
||||
budgets: dict[str, Budget]
|
||||
category_details: list[Category]
|
||||
budgets: list[Budget]
|
||||
bills: list[Bill]
|
||||
primary_currency: Currency
|
||||
|
||||
@@ -142,10 +142,10 @@ class FireflyDataUpdateCoordinator(DataUpdateCoordinator[FireflyCoordinatorData]
|
||||
) from err
|
||||
|
||||
return FireflyCoordinatorData(
|
||||
accounts={account.id: account for account in accounts},
|
||||
accounts=accounts,
|
||||
categories=categories,
|
||||
category_details={category.id: category for category in category_details},
|
||||
budgets={budget.id: budget for budget in budgets},
|
||||
category_details=category_details,
|
||||
budgets=budgets,
|
||||
bills=bills,
|
||||
primary_currency=primary_currency,
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ class FireflyAccountBaseEntity(FireflyBaseEntity):
|
||||
) -> None:
|
||||
"""Initialize a Firefly account entity."""
|
||||
super().__init__(coordinator)
|
||||
self._account_id = account.id
|
||||
self._account = account
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer=MANUFACTURER,
|
||||
@@ -58,10 +58,6 @@ class FireflyAccountBaseEntity(FireflyBaseEntity):
|
||||
f"{coordinator.config_entry.unique_id}_account_{account.id}_{key}"
|
||||
)
|
||||
|
||||
@property
|
||||
def _account(self) -> Account:
|
||||
return self.coordinator.data.accounts[self._account_id]
|
||||
|
||||
|
||||
class FireflyCategoryBaseEntity(FireflyBaseEntity):
|
||||
"""Base class for Firefly III category entity."""
|
||||
@@ -74,7 +70,7 @@ class FireflyCategoryBaseEntity(FireflyBaseEntity):
|
||||
) -> None:
|
||||
"""Initialize a Firefly category entity."""
|
||||
super().__init__(coordinator)
|
||||
self._category_id = category.id
|
||||
self._category = category
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer=MANUFACTURER,
|
||||
@@ -88,10 +84,6 @@ class FireflyCategoryBaseEntity(FireflyBaseEntity):
|
||||
f"{coordinator.config_entry.unique_id}_category_{category.id}_{key}"
|
||||
)
|
||||
|
||||
@property
|
||||
def _category(self) -> Category:
|
||||
return self.coordinator.data.category_details[self._category_id]
|
||||
|
||||
|
||||
class FireflyBudgetBaseEntity(FireflyBaseEntity):
|
||||
"""Base class for Firefly III budget entity."""
|
||||
@@ -104,7 +96,7 @@ class FireflyBudgetBaseEntity(FireflyBaseEntity):
|
||||
) -> None:
|
||||
"""Initialize a Firefly budget entity."""
|
||||
super().__init__(coordinator)
|
||||
self._budget_id = budget.id
|
||||
self._budget = budget
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer=MANUFACTURER,
|
||||
@@ -117,7 +109,3 @@ class FireflyBudgetBaseEntity(FireflyBaseEntity):
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.config_entry.unique_id}_budget_{budget.id}_{key}"
|
||||
)
|
||||
|
||||
@property
|
||||
def _budget(self) -> Budget:
|
||||
return self.coordinator.data.budgets[self._budget_id]
|
||||
|
||||
@@ -51,7 +51,7 @@ async def async_setup_entry(
|
||||
coordinator = entry.runtime_data
|
||||
entities: list[SensorEntity] = []
|
||||
|
||||
for account in coordinator.data.accounts.values():
|
||||
for account in coordinator.data.accounts:
|
||||
entities.append(
|
||||
FireflyAccountBalanceSensor(coordinator, account, ACCOUNT_BALANCE)
|
||||
)
|
||||
@@ -61,14 +61,14 @@ async def async_setup_entry(
|
||||
entities.extend(
|
||||
[
|
||||
FireflyCategorySensor(coordinator, category, CATEGORY)
|
||||
for category in coordinator.data.category_details.values()
|
||||
for category in coordinator.data.category_details
|
||||
]
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
[
|
||||
FireflyBudgetSensor(coordinator, budget, BUDGET)
|
||||
for budget in coordinator.data.budgets.values()
|
||||
for budget in coordinator.data.budgets
|
||||
]
|
||||
)
|
||||
|
||||
@@ -90,6 +90,7 @@ class FireflyAccountBalanceSensor(FireflyAccountBaseEntity, SensorEntity):
|
||||
) -> None:
|
||||
"""Initialize the account balance sensor."""
|
||||
super().__init__(coordinator, account, key)
|
||||
self._account = account
|
||||
self._attr_native_unit_of_measurement = (
|
||||
coordinator.data.primary_currency.attributes.code
|
||||
)
|
||||
@@ -107,6 +108,16 @@ class FireflyAccountRoleSensor(FireflyAccountBaseEntity, SensorEntity):
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_entity_registry_enabled_default = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: FireflyDataUpdateCoordinator,
|
||||
account: Account,
|
||||
key: str,
|
||||
) -> None:
|
||||
"""Initialize the account role sensor."""
|
||||
super().__init__(coordinator, account, key)
|
||||
self._account = account
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return account role."""
|
||||
@@ -162,6 +173,7 @@ class FireflyCategorySensor(FireflyCategoryBaseEntity, SensorEntity):
|
||||
) -> None:
|
||||
"""Initialize the category sensor."""
|
||||
super().__init__(coordinator, category, key)
|
||||
self._category = category
|
||||
self._attr_native_unit_of_measurement = (
|
||||
coordinator.data.primary_currency.attributes.code
|
||||
)
|
||||
@@ -193,6 +205,7 @@ class FireflyBudgetSensor(FireflyBudgetBaseEntity, SensorEntity):
|
||||
) -> None:
|
||||
"""Initialize the budget sensor."""
|
||||
super().__init__(coordinator, budget, key)
|
||||
self._budget = budget
|
||||
self._attr_native_unit_of_measurement = (
|
||||
coordinator.data.primary_currency.attributes.code
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ from fritzconnection.core.exceptions import (
|
||||
FritzSecurityError,
|
||||
FritzServiceError,
|
||||
)
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
@@ -69,7 +68,6 @@ BUTTON_TYPE_WOL = "WakeOnLan"
|
||||
UPTIME_DEVIATION = 5
|
||||
|
||||
FRITZ_EXCEPTIONS = (
|
||||
ConnectionError,
|
||||
FritzActionError,
|
||||
FritzActionFailedError,
|
||||
FritzConnectionException,
|
||||
|
||||
@@ -26,7 +26,7 @@ from .const import (
|
||||
MeshRoles,
|
||||
)
|
||||
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
|
||||
from .entity import FritzBoxBaseEntity
|
||||
from .entity import FritzBoxBaseEntity, FritzDeviceBase
|
||||
from .helpers import device_filter_out_from_trackers
|
||||
from .models import FritzDevice, SwitchInfo
|
||||
|
||||
@@ -332,7 +332,7 @@ class FritzBoxBaseSwitch(FritzBoxBaseEntity, SwitchEntity):
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return icon."""
|
||||
"""Return name."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
@@ -485,51 +485,42 @@ class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class FritzBoxProfileSwitch(FritzBoxBaseCoordinatorSwitch):
|
||||
class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
|
||||
"""Defines a FRITZ!Box Tools DeviceProfile switch."""
|
||||
|
||||
_attr_translation_key = "internet_access"
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None:
|
||||
"""Init Fritz profile."""
|
||||
self._mac = device.mac_address
|
||||
description = SwitchEntityDescription(
|
||||
key=f"{self._mac}_internet_access",
|
||||
)
|
||||
super().__init__(avm_wrapper, device.hostname, description)
|
||||
super().__init__(avm_wrapper, device)
|
||||
self._attr_is_on: bool = False
|
||||
self._attr_unique_id = f"{self._mac}_internet_access"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device information."""
|
||||
return DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, self._mac)},
|
||||
)
|
||||
|
||||
@property
|
||||
def _device(self) -> FritzDevice:
|
||||
"""Return the device for this profile switch."""
|
||||
return self.coordinator.devices[self._mac]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return availability of the switch."""
|
||||
if self._device.wan_access is None:
|
||||
return False
|
||||
return self.coordinator.last_update_success
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Switch status."""
|
||||
return self._device.wan_access
|
||||
return self._avm_wrapper.devices[self._mac].wan_access
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return availability of the switch."""
|
||||
if self._avm_wrapper.devices[self._mac].wan_access is None:
|
||||
return False
|
||||
return super().available
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on switch."""
|
||||
await self._async_handle_turn_on_off(turn_on=True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off switch."""
|
||||
await self._async_handle_turn_on_off(turn_on=False)
|
||||
|
||||
async def _async_handle_turn_on_off(self, turn_on: bool) -> None:
|
||||
"""Handle switch state change request."""
|
||||
await self.coordinator.async_set_allow_wan_access(
|
||||
self._device.ip_address, turn_on
|
||||
)
|
||||
self._device.wan_access = turn_on
|
||||
await self._avm_wrapper.async_set_allow_wan_access(self.ip_address, turn_on)
|
||||
self._avm_wrapper.devices[self._mac].wan_access = turn_on
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyfronius"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["PyFronius==0.8.2"]
|
||||
"requirements": ["PyFronius==0.8.0"]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"preview_features": { "retro": {}, "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260312.1"]
|
||||
"requirements": ["home-assistant-frontend==20260312.0"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"preview_features": {
|
||||
"retro": {
|
||||
"description": "Transforms your dashboard with a nostalgic retro look.",
|
||||
"disable_confirmation": "Your dashboard will return to its normal look. You can re-enable this at any time in Labs settings.",
|
||||
"enable_confirmation": "Your dashboard will be transformed with a retro theme. You can turn this off at any time in Labs settings.",
|
||||
"name": "Retro"
|
||||
},
|
||||
"winter_mode": {
|
||||
"description": "Adds falling snowflakes on your screen. Get your home ready for winter! ❄️\n\nIf you have animations disabled in your device accessibility settings, this feature will not work.",
|
||||
"disable_confirmation": "Snowflakes will no longer fall on your screen. You can re-enable this at any time in Labs settings.",
|
||||
|
||||
@@ -116,11 +116,7 @@ async def _validate_config(
|
||||
|
||||
|
||||
CONFIG_FLOW = {
|
||||
"user": SchemaFlowFormStep(
|
||||
vol.Schema(CONFIG_SCHEMA),
|
||||
validate_user_input=_validate_config,
|
||||
next_step="presets",
|
||||
),
|
||||
"user": SchemaFlowFormStep(vol.Schema(CONFIG_SCHEMA), next_step="presets"),
|
||||
"presets": SchemaFlowFormStep(vol.Schema(PRESETS_SCHEMA)),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"min_max_runtime": "Minimum run time must be less than the maximum run time."
|
||||
},
|
||||
"step": {
|
||||
"presets": {
|
||||
"data": {
|
||||
@@ -48,7 +45,7 @@
|
||||
},
|
||||
"options": {
|
||||
"error": {
|
||||
"min_max_runtime": "[%key:component::generic_thermostat::config::error::min_max_runtime%]"
|
||||
"min_max_runtime": "Minimum run time must be less than the maximum run time."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
@@ -93,7 +90,7 @@
|
||||
"services": {
|
||||
"reload": {
|
||||
"description": "Reloads generic thermostats from the YAML-configuration.",
|
||||
"name": "Reload generic thermostats"
|
||||
"name": "[%key:common::action::reload%]"
|
||||
}
|
||||
},
|
||||
"title": "Generic thermostat"
|
||||
|
||||
@@ -113,7 +113,7 @@ class GoogleTravelTimeSensor(SensorEntity):
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, api_key)},
|
||||
name=DEFAULT_NAME,
|
||||
name=DOMAIN,
|
||||
)
|
||||
|
||||
self._config_entry = config_entry
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
"""Diagnostics support for Google Weather."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_REFERRER
|
||||
from .coordinator import GoogleWeatherConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
CONF_API_KEY,
|
||||
CONF_REFERRER,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: GoogleWeatherConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
diag_data: dict[str, Any] = {
|
||||
"entry": entry.as_dict(),
|
||||
"subentries": {},
|
||||
}
|
||||
|
||||
for subentry_id, subentry_rt in entry.runtime_data.subentries_runtime_data.items():
|
||||
diag_data["subentries"][subentry_id] = {
|
||||
"observation_data": subentry_rt.coordinator_observation.data.to_dict()
|
||||
if subentry_rt.coordinator_observation.data
|
||||
else None,
|
||||
"daily_forecast_data": subentry_rt.coordinator_daily_forecast.data.to_dict()
|
||||
if subentry_rt.coordinator_daily_forecast.data
|
||||
else None,
|
||||
"hourly_forecast_data": subentry_rt.coordinator_hourly_forecast.data.to_dict()
|
||||
if subentry_rt.coordinator_hourly_forecast.data
|
||||
else None,
|
||||
}
|
||||
|
||||
return async_redact_data(diag_data, TO_REDACT)
|
||||
@@ -43,7 +43,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: No discovery.
|
||||
|
||||
@@ -98,7 +98,7 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]):
|
||||
"""Set light color in kelvin."""
|
||||
await device.set_temperature(temperature)
|
||||
|
||||
async def set_scene(self, device: GoveeDevice, scene: str) -> None:
|
||||
async def set_scene(self, device: GoveeController, scene: str) -> None:
|
||||
"""Set light scene."""
|
||||
await device.set_scene(scene)
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
|
||||
|
||||
super().__init__(coordinator)
|
||||
self._device = device
|
||||
device.set_update_callback(self._update_callback)
|
||||
|
||||
self._attr_unique_id = device.fingerprint
|
||||
|
||||
@@ -193,20 +194,9 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
|
||||
await self.coordinator.turn_off(self._device)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register update callback when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
self._device.set_update_callback(self._update_callback)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Unregister update callback when entity is removed."""
|
||||
self._device.set_update_callback(None)
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
@callback
|
||||
def _update_callback(self, device: GoveeDevice) -> None:
|
||||
if self.hass:
|
||||
self.async_write_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _save_last_color_state(self) -> None:
|
||||
color_mode = self.color_mode
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["greenplanet-energy-api==0.1.10"],
|
||||
"requirements": ["greenplanet-energy-api==0.1.4"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from dataclasses import replace
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import os
|
||||
@@ -16,7 +15,6 @@ from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor.models import (
|
||||
GreenOptions,
|
||||
HomeAssistantInfo,
|
||||
HomeAssistantOptions,
|
||||
HostInfo,
|
||||
InstalledAddon,
|
||||
NetworkInfo,
|
||||
@@ -24,28 +22,20 @@ from aiohasupervisor.models import (
|
||||
RootInfo,
|
||||
StoreInfo,
|
||||
SupervisorInfo,
|
||||
SupervisorOptions,
|
||||
YellowOptions,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.auth.models import RefreshToken
|
||||
from homeassistant.components import frontend, panel_custom
|
||||
from homeassistant.components.homeassistant import async_set_stop_handler
|
||||
from homeassistant.components.http import (
|
||||
CONF_SERVER_HOST,
|
||||
CONF_SERVER_PORT,
|
||||
CONF_SSL_CERTIFICATE,
|
||||
StaticPathConfig,
|
||||
)
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_NAME,
|
||||
EVENT_CORE_CONFIG_UPDATE,
|
||||
HASSIO_USER_NAME,
|
||||
SERVER_PORT,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
@@ -455,30 +445,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
require_admin=True,
|
||||
)
|
||||
|
||||
async def update_hass_api(http_config: dict[str, Any], refresh_token: RefreshToken):
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
options = HomeAssistantOptions(
|
||||
ssl=CONF_SSL_CERTIFICATE in http_config,
|
||||
port=http_config.get(CONF_SERVER_PORT) or SERVER_PORT,
|
||||
refresh_token=refresh_token.token,
|
||||
)
|
||||
|
||||
if http_config.get(CONF_SERVER_HOST) is not None:
|
||||
options = replace(options, watchdog=False)
|
||||
_LOGGER.warning(
|
||||
"Found incompatible HTTP option 'server_host'. Watchdog feature"
|
||||
" disabled"
|
||||
)
|
||||
|
||||
try:
|
||||
await supervisor_client.homeassistant.set_options(options)
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning(
|
||||
"Failed to update Home Assistant options in Supervisor: %s", err
|
||||
)
|
||||
|
||||
update_hass_api_task = hass.async_create_task(
|
||||
update_hass_api(config.get("http", {}), refresh_token), eager_start=True
|
||||
hassio.update_hass_api(config.get("http", {}), refresh_token), eager_start=True
|
||||
)
|
||||
|
||||
last_timezone = None
|
||||
@@ -489,25 +457,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
nonlocal last_timezone
|
||||
nonlocal last_country
|
||||
|
||||
new_timezone = hass.config.time_zone
|
||||
new_country = hass.config.country
|
||||
new_timezone = str(hass.config.time_zone)
|
||||
new_country = str(hass.config.country)
|
||||
|
||||
if new_timezone != last_timezone or new_country != last_country:
|
||||
last_timezone = new_timezone
|
||||
last_country = new_country
|
||||
|
||||
try:
|
||||
await supervisor_client.supervisor.set_options(
|
||||
SupervisorOptions(timezone=new_timezone, country=new_country)
|
||||
)
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning("Failed to update Supervisor options: %s", err)
|
||||
await hassio.update_hass_config(new_timezone, new_country)
|
||||
|
||||
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config)
|
||||
|
||||
push_config_task = hass.async_create_task(push_config(None), eager_start=True)
|
||||
# Start listening for problems with supervisor and making issues
|
||||
hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass)
|
||||
hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass, hassio)
|
||||
issues_task = hass.async_create_task(issues.setup(), eager_start=True)
|
||||
|
||||
async def async_service_handler(service: ServiceCall) -> None:
|
||||
@@ -655,7 +617,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
async_set_stop_handler(hass, _async_stop)
|
||||
|
||||
# Init discovery Hass.io feature
|
||||
async_setup_discovery_view(hass)
|
||||
async_setup_discovery_view(hass, hassio)
|
||||
|
||||
# Init auth Hass.io feature
|
||||
assert user is not None
|
||||
|
||||
@@ -21,15 +21,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
|
||||
from .const import ATTR_ADDON, ATTR_UUID, DOMAIN
|
||||
from .handler import get_supervisor_client
|
||||
from .handler import HassIO, get_supervisor_client
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_discovery_view(hass: HomeAssistant) -> None:
|
||||
def async_setup_discovery_view(hass: HomeAssistant, hassio: HassIO) -> None:
|
||||
"""Discovery setup."""
|
||||
hassio_discovery = HassIODiscovery(hass)
|
||||
hassio_discovery = HassIODiscovery(hass, hassio)
|
||||
supervisor_client = get_supervisor_client(hass)
|
||||
hass.http.register_view(hassio_discovery)
|
||||
|
||||
@@ -77,9 +77,10 @@ class HassIODiscovery(HomeAssistantView):
|
||||
name = "api:hassio_push:discovery"
|
||||
url = "/api/hassio_push/discovery/{uuid}"
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
def __init__(self, hass: HomeAssistant, hassio: HassIO) -> None:
|
||||
"""Initialize WebView."""
|
||||
self.hass = hass
|
||||
self.hassio = hassio
|
||||
self._supervisor_client = get_supervisor_client(hass)
|
||||
|
||||
async def post(self, request: web.Request, uuid: str) -> web.Response:
|
||||
|
||||
@@ -14,6 +14,13 @@ from aiohasupervisor.models import SupervisorOptions
|
||||
import aiohttp
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.auth.models import RefreshToken
|
||||
from homeassistant.components.http import (
|
||||
CONF_SERVER_HOST,
|
||||
CONF_SERVER_PORT,
|
||||
CONF_SSL_CERTIFICATE,
|
||||
)
|
||||
from homeassistant.const import SERVER_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
|
||||
@@ -28,6 +35,22 @@ class HassioAPIError(RuntimeError):
|
||||
"""Return if a API trow a error."""
|
||||
|
||||
|
||||
def _api_bool[**_P](
|
||||
funct: Callable[_P, Coroutine[Any, Any, dict[str, Any]]],
|
||||
) -> Callable[_P, Coroutine[Any, Any, bool]]:
|
||||
"""Return a boolean."""
|
||||
|
||||
async def _wrapper(*argv: _P.args, **kwargs: _P.kwargs) -> bool:
|
||||
"""Wrap function."""
|
||||
try:
|
||||
data = await funct(*argv, **kwargs)
|
||||
return data["result"] == "ok"
|
||||
except HassioAPIError:
|
||||
return False
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
def api_data[**_P](
|
||||
funct: Callable[_P, Coroutine[Any, Any, dict[str, Any]]],
|
||||
) -> Callable[_P, Coroutine[Any, Any, Any]]:
|
||||
@@ -72,6 +95,37 @@ class HassIO:
|
||||
"""
|
||||
return self.send_command("/ingress/panels", method="get")
|
||||
|
||||
@_api_bool
|
||||
async def update_hass_api(
|
||||
self, http_config: dict[str, Any], refresh_token: RefreshToken
|
||||
):
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
port = http_config.get(CONF_SERVER_PORT) or SERVER_PORT
|
||||
options = {
|
||||
"ssl": CONF_SSL_CERTIFICATE in http_config,
|
||||
"port": port,
|
||||
"refresh_token": refresh_token.token,
|
||||
}
|
||||
|
||||
if http_config.get(CONF_SERVER_HOST) is not None:
|
||||
options["watchdog"] = False
|
||||
_LOGGER.warning(
|
||||
"Found incompatible HTTP option 'server_host'. Watchdog feature"
|
||||
" disabled"
|
||||
)
|
||||
|
||||
return await self.send_command("/homeassistant/options", payload=options)
|
||||
|
||||
@_api_bool
|
||||
def update_hass_config(self, timezone: str, country: str | None) -> Coroutine:
|
||||
"""Update Home-Assistant timezone data on Hass.io.
|
||||
|
||||
This method returns a coroutine.
|
||||
"""
|
||||
return self.send_command(
|
||||
"/supervisor/options", payload={"timezone": timezone, "country": country}
|
||||
)
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str,
|
||||
|
||||
@@ -63,7 +63,7 @@ from .const import (
|
||||
UPDATE_KEY_SUPERVISOR,
|
||||
)
|
||||
from .coordinator import HassioDataUpdateCoordinator, get_addons_list, get_host_info
|
||||
from .handler import get_supervisor_client
|
||||
from .handler import HassIO, get_supervisor_client
|
||||
|
||||
ISSUE_KEY_UNHEALTHY = "unhealthy"
|
||||
ISSUE_KEY_UNSUPPORTED = "unsupported"
|
||||
@@ -175,9 +175,10 @@ class Issue:
|
||||
class SupervisorIssues:
|
||||
"""Create issues from supervisor events."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
def __init__(self, hass: HomeAssistant, client: HassIO) -> None:
|
||||
"""Initialize supervisor issues."""
|
||||
self._hass = hass
|
||||
self._client = client
|
||||
self._unsupported_reasons: set[str] = set()
|
||||
self._unhealthy_reasons: set[str] = set()
|
||||
self._issues: dict[UUID, Issue] = {}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hassio",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["aiohasupervisor==0.4.3"],
|
||||
"requirements": ["aiohasupervisor==0.4.2"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -211,10 +211,6 @@
|
||||
"description": "System is currently unhealthy because Docker is configured incorrectly. For troubleshooting information, select Learn more.",
|
||||
"title": "Unhealthy system - Docker misconfigured"
|
||||
},
|
||||
"unhealthy_docker_gateway_unprotected": {
|
||||
"description": "System is currently unhealthy because Supervisor was not able to apply firewall protection for the Docker gateway IP. For troubleshooting information, select Learn more.",
|
||||
"title": "Unhealthy system - Docker gateway unprotected"
|
||||
},
|
||||
"unhealthy_duplicate_os_installation": {
|
||||
"description": "System is currently unhealthy because it has detected multiple Home Assistant OS installations. For troubleshooting information, select Learn more.",
|
||||
"title": "Unhealthy system - Duplicate Home Assistant OS installation"
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"services": {
|
||||
"reload": {
|
||||
"description": "Reloads history stats sensors from the YAML-configuration.",
|
||||
"name": "Reload history stats sensors"
|
||||
"name": "[%key:common::action::reload%]"
|
||||
}
|
||||
},
|
||||
"title": "History Stats"
|
||||
|
||||
@@ -312,7 +312,6 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
|
||||
case EventType.NOTIFY:
|
||||
settings = self.data.settings
|
||||
events = self.data.events
|
||||
program_update_event_value = None
|
||||
for event in event_message.data.items:
|
||||
event_key = event.key
|
||||
if event_key in SettingKey.__members__.values(): # type: ignore[comparison-overlap]
|
||||
@@ -331,13 +330,11 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
) and isinstance(event_value, str):
|
||||
program_update_event_value = ProgramKey(event_value)
|
||||
await self.update_options(
|
||||
event_key,
|
||||
ProgramKey(event_value),
|
||||
)
|
||||
events[event_key] = event
|
||||
# Process program update after all events to ensure
|
||||
# BSH_COMMON_OPTION_BASE_PROGRAM event is available for
|
||||
# favorite program resolution
|
||||
if program_update_event_value:
|
||||
await self.update_options(program_update_event_value)
|
||||
self._call_event_listener(event_message)
|
||||
|
||||
case EventType.EVENT:
|
||||
@@ -496,7 +493,7 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
|
||||
programs = []
|
||||
events = {}
|
||||
options = {}
|
||||
if appliance.type in APPLIANCES_WITH_PROGRAMS: # pylint: disable=too-many-nested-blocks
|
||||
if appliance.type in APPLIANCES_WITH_PROGRAMS:
|
||||
try:
|
||||
all_programs = await self.client.get_all_programs(appliance.ha_id)
|
||||
except TooManyRequestsError:
|
||||
@@ -532,17 +529,6 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
|
||||
)
|
||||
current_program_key = program.key
|
||||
program_options = program.options
|
||||
if (
|
||||
current_program_key == ProgramKey.BSH_COMMON_FAVORITE_001
|
||||
and program_options
|
||||
):
|
||||
# The API doesn't allow to fetch the options from the favorite program.
|
||||
# We can attempt to get the base program and get the options
|
||||
for option in program_options:
|
||||
if option.key == OptionKey.BSH_COMMON_BASE_PROGRAM:
|
||||
current_program_key = ProgramKey(option.value)
|
||||
break
|
||||
|
||||
if current_program_key:
|
||||
options = await self.get_options_definitions(current_program_key)
|
||||
for option in program_options or []:
|
||||
@@ -609,24 +595,15 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
|
||||
)
|
||||
return {}
|
||||
|
||||
async def update_options(self, program_key: ProgramKey) -> None:
|
||||
async def update_options(
|
||||
self, event_key: EventKey, program_key: ProgramKey
|
||||
) -> None:
|
||||
"""Update options for appliance."""
|
||||
options = self.data.options
|
||||
events = self.data.events
|
||||
options_to_notify = options.copy()
|
||||
options.clear()
|
||||
if (
|
||||
program_key == ProgramKey.BSH_COMMON_FAVORITE_001
|
||||
and (event := events.get(EventKey.BSH_COMMON_OPTION_BASE_PROGRAM))
|
||||
and isinstance(event.value, str)
|
||||
):
|
||||
# The API doesn't allow to fetch the options from the favorite program.
|
||||
# We can attempt to get the base program and get the options
|
||||
resolved_program_key = ProgramKey(event.value)
|
||||
else:
|
||||
resolved_program_key = program_key
|
||||
|
||||
options.update(await self.get_options_definitions(resolved_program_key))
|
||||
options.update(await self.get_options_definitions(program_key))
|
||||
|
||||
for option in options.values():
|
||||
option_value = option.constraints.default if option.constraints else None
|
||||
|
||||
@@ -430,24 +430,11 @@ class HomeConnectProgramSelectEntity(HomeConnectEntity, SelectEntity):
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the program value."""
|
||||
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
|
||||
program_key = (
|
||||
ProgramKey(event_value)
|
||||
self._attr_current_option = (
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP.get(ProgramKey(event_value))
|
||||
if event and isinstance(event_value := event.value, str)
|
||||
else None
|
||||
)
|
||||
if (
|
||||
program_key == ProgramKey.BSH_COMMON_FAVORITE_001
|
||||
and (
|
||||
base_program_event := self.appliance.events.get(
|
||||
EventKey.BSH_COMMON_OPTION_BASE_PROGRAM
|
||||
)
|
||||
)
|
||||
and isinstance(base_program_event.value, str)
|
||||
):
|
||||
program_key = ProgramKey(base_program_event.value)
|
||||
self._attr_current_option = (
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP.get(program_key) if program_key else None
|
||||
)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select new program."""
|
||||
|
||||
@@ -208,11 +208,11 @@
|
||||
"services": {
|
||||
"check_config": {
|
||||
"description": "Checks the Home Assistant YAML-configuration files for errors. Errors will be shown in the Home Assistant logs.",
|
||||
"name": "Check Home Assistant configuration"
|
||||
"name": "Check configuration"
|
||||
},
|
||||
"reload_all": {
|
||||
"description": "Reloads all YAML configuration that can be reloaded without restarting Home Assistant.",
|
||||
"name": "Reload all Home Assistant configuration"
|
||||
"name": "Reload all"
|
||||
},
|
||||
"reload_config_entry": {
|
||||
"description": "Reloads the specified config entry.",
|
||||
@@ -240,7 +240,7 @@
|
||||
"name": "Safe mode"
|
||||
}
|
||||
},
|
||||
"name": "Restart Home Assistant"
|
||||
"name": "[%key:common::action::restart%]"
|
||||
},
|
||||
"save_persistent_states": {
|
||||
"description": "Saves the persistent states immediately. Maintains the normal periodic saving interval.",
|
||||
@@ -262,11 +262,11 @@
|
||||
"name": "[%key:common::config_flow::data::longitude%]"
|
||||
}
|
||||
},
|
||||
"name": "Set Home Assistant location"
|
||||
"name": "Set location"
|
||||
},
|
||||
"stop": {
|
||||
"description": "Stops Home Assistant.",
|
||||
"name": "Stop Home Assistant"
|
||||
"name": "[%key:common::action::stop%]"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Generic action to toggle devices on/off under any domain.",
|
||||
|
||||
@@ -5,8 +5,6 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
|
||||
from universal_silabs_flasher.flasher import Zbt2Flasher
|
||||
|
||||
from homeassistant.components import usb
|
||||
from homeassistant.components.homeassistant_hardware import firmware_config_flow
|
||||
from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
@@ -15,6 +13,7 @@ from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
ResetTarget,
|
||||
)
|
||||
from homeassistant.components.usb import (
|
||||
usb_service_info_from_device,
|
||||
@@ -77,7 +76,15 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
context: ConfigFlowContext
|
||||
|
||||
ZIGBEE_BAUDRATE = 460800
|
||||
_flasher_cls = Zbt2Flasher
|
||||
|
||||
# Early ZBT-2 samples used RTS/DTR to trigger the bootloader, later ones use the
|
||||
# baudrate method. Since the two are mutually exclusive we just use both.
|
||||
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE]
|
||||
APPLICATION_PROBE_METHODS = [
|
||||
(ApplicationType.GECKO_BOOTLOADER, 115200),
|
||||
(ApplicationType.EZSP, ZIGBEE_BAUDRATE),
|
||||
(ApplicationType.SPINEL, 460800),
|
||||
]
|
||||
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
||||
@@ -4,8 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.flasher import Zbt2Flasher
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
@@ -25,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeAssistantConnectZBT2ConfigEntry
|
||||
from .config_flow import ZBT2FirmwareMixin
|
||||
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, HARDWARE_NAME, SERIAL_NUMBER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -135,7 +134,8 @@ async def async_setup_entry(
|
||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Connect ZBT-2 firmware update entity."""
|
||||
|
||||
_flasher_cls = Zbt2Flasher
|
||||
BOOTLOADER_RESET_METHODS = ZBT2FirmwareMixin.BOOTLOADER_RESET_METHODS
|
||||
APPLICATION_PROBE_METHODS = ZBT2FirmwareMixin.APPLICATION_PROBE_METHODS
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -12,7 +12,6 @@ from aiohttp import ClientError
|
||||
from ha_silabs_firmware_client import FirmwareUpdateClient, ManifestMissing
|
||||
from universal_silabs_flasher.common import Version
|
||||
from universal_silabs_flasher.firmware import NabuCasaMetadata
|
||||
from universal_silabs_flasher.flasher import DeviceSpecificFlasher
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
AddonError,
|
||||
@@ -40,6 +39,7 @@ from .util import (
|
||||
FirmwareInfo,
|
||||
OwningAddon,
|
||||
OwningIntegration,
|
||||
ResetTarget,
|
||||
async_firmware_flashing_context,
|
||||
async_flash_silabs_firmware,
|
||||
get_otbr_addon_manager,
|
||||
@@ -81,7 +81,8 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Base flow to install firmware."""
|
||||
|
||||
ZIGBEE_BAUDRATE = 115200 # Default, subclasses may override
|
||||
_flasher_cls: type[DeviceSpecificFlasher]
|
||||
BOOTLOADER_RESET_METHODS: list[ResetTarget] = [] # Default, subclasses may override
|
||||
APPLICATION_PROBE_METHODS: list[tuple[ApplicationType, int]] = []
|
||||
|
||||
_picked_firmware_type: PickedFirmwareType
|
||||
_zigbee_flow_strategy: ZigbeeFlowStrategy = ZigbeeFlowStrategy.RECOMMENDED
|
||||
@@ -238,7 +239,8 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
# isn't strictly necessary for functionality.
|
||||
self._probed_firmware_info = await probe_silabs_firmware_info(
|
||||
self._device,
|
||||
flasher_cls=self._flasher_cls,
|
||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||
)
|
||||
|
||||
firmware_install_required = self._probed_firmware_info is None or (
|
||||
@@ -309,8 +311,9 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
hass=self.hass,
|
||||
device=self._device,
|
||||
fw_data=fw_data,
|
||||
flasher_cls=self._flasher_cls,
|
||||
expected_installed_firmware_type=expected_installed_firmware_type,
|
||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||
progress_callback=lambda offset, total: self.async_update_progress(
|
||||
offset / total
|
||||
),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
"serialx==0.6.2",
|
||||
"universal-silabs-flasher==1.0.3",
|
||||
"universal-silabs-flasher==0.1.2",
|
||||
"ha-silabs-firmware-client==0.3.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from ha_silabs_firmware_client import FirmwareManifest, FirmwareMetadata
|
||||
from universal_silabs_flasher.flasher import DeviceSpecificFlasher
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.update import (
|
||||
@@ -26,6 +25,7 @@ from .helpers import async_register_firmware_info_callback
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
ResetTarget,
|
||||
async_firmware_flashing_context,
|
||||
async_flash_silabs_firmware,
|
||||
)
|
||||
@@ -87,11 +87,13 @@ class BaseFirmwareUpdateEntity(
|
||||
|
||||
# Subclasses provide the mapping between firmware types and entity descriptions
|
||||
entity_description: FirmwareUpdateEntityDescription
|
||||
BOOTLOADER_RESET_METHODS: list[ResetTarget]
|
||||
APPLICATION_PROBE_METHODS: list[tuple[ApplicationType, int]]
|
||||
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
|
||||
)
|
||||
_attr_has_entity_name = True
|
||||
_flasher_cls: type[DeviceSpecificFlasher]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -280,8 +282,9 @@ class BaseFirmwareUpdateEntity(
|
||||
hass=self.hass,
|
||||
device=self._current_device,
|
||||
fw_data=fw_data,
|
||||
flasher_cls=self._flasher_cls,
|
||||
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
|
||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||
progress_callback=self._update_progress,
|
||||
)
|
||||
finally:
|
||||
|
||||
@@ -10,9 +10,12 @@ from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType
|
||||
from universal_silabs_flasher.const import (
|
||||
ApplicationType as FlasherApplicationType,
|
||||
ResetTarget as FlasherResetTarget,
|
||||
)
|
||||
from universal_silabs_flasher.firmware import parse_firmware_image
|
||||
from universal_silabs_flasher.flasher import BaseFlasher, DeviceSpecificFlasher, Flasher
|
||||
from universal_silabs_flasher.flasher import Flasher
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@@ -60,6 +63,18 @@ class ApplicationType(StrEnum):
|
||||
return FlasherApplicationType(self.value)
|
||||
|
||||
|
||||
class ResetTarget(StrEnum):
|
||||
"""Methods to reset a device into bootloader mode."""
|
||||
|
||||
RTS_DTR = "rts_dtr"
|
||||
BAUDRATE = "baudrate"
|
||||
YELLOW = "yellow"
|
||||
|
||||
def as_flasher_reset_target(self) -> FlasherResetTarget:
|
||||
"""Convert the reset target enum into one compatible with USF."""
|
||||
return FlasherResetTarget(self.value)
|
||||
|
||||
|
||||
@singleton(OTBR_ADDON_MANAGER_DATA)
|
||||
@callback
|
||||
def get_otbr_addon_manager(hass: HomeAssistant) -> WaitingAddonManager:
|
||||
@@ -295,20 +310,23 @@ async def guess_firmware_info(hass: HomeAssistant, device_path: str) -> Firmware
|
||||
async def probe_silabs_firmware_info(
|
||||
device: str,
|
||||
*,
|
||||
flasher_cls: type[BaseFlasher],
|
||||
application_probe_methods: Sequence[ApplicationType] | None = None,
|
||||
bootloader_reset_methods: Sequence[ResetTarget],
|
||||
application_probe_methods: Sequence[tuple[ApplicationType, int]],
|
||||
) -> FirmwareInfo | None:
|
||||
"""Probe the running firmware on a SiLabs device."""
|
||||
flasher = flasher_cls(device=device)
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=tuple(
|
||||
(m.as_flasher_application_type(), baudrate)
|
||||
for m, baudrate in application_probe_methods
|
||||
),
|
||||
bootloader_reset=tuple(
|
||||
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
await flasher.probe_app_type(
|
||||
only=(
|
||||
[m.as_flasher_application_type() for m in application_probe_methods]
|
||||
if application_probe_methods is not None
|
||||
else None
|
||||
)
|
||||
)
|
||||
await flasher.probe_app_type()
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.debug("Failed to probe application type", exc_info=True)
|
||||
|
||||
@@ -331,25 +349,20 @@ async def probe_silabs_firmware_info(
|
||||
async def probe_silabs_firmware_type(
|
||||
device: str,
|
||||
*,
|
||||
bootloader_reset_methods: Sequence[ResetTarget],
|
||||
application_probe_methods: Sequence[tuple[ApplicationType, int]],
|
||||
) -> ApplicationType | None:
|
||||
"""Probe the running firmware type on a SiLabs device."""
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=[
|
||||
(m.as_flasher_application_type(), b) for m, b in application_probe_methods
|
||||
],
|
||||
|
||||
fw_info = await probe_silabs_firmware_info(
|
||||
device,
|
||||
bootloader_reset_methods=bootloader_reset_methods,
|
||||
application_probe_methods=application_probe_methods,
|
||||
)
|
||||
|
||||
try:
|
||||
await flasher.probe_app_type()
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.debug("Failed to probe application type", exc_info=True)
|
||||
|
||||
if flasher.app_type is None:
|
||||
if fw_info is None:
|
||||
return None
|
||||
|
||||
return ApplicationType.from_flasher_application_type(flasher.app_type)
|
||||
return fw_info.firmware_type
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -372,18 +385,36 @@ async def async_flash_silabs_firmware(
|
||||
hass: HomeAssistant,
|
||||
device: str,
|
||||
fw_data: bytes,
|
||||
flasher_cls: type[DeviceSpecificFlasher],
|
||||
expected_installed_firmware_type: ApplicationType,
|
||||
bootloader_reset_methods: Sequence[ResetTarget],
|
||||
application_probe_methods: Sequence[tuple[ApplicationType, int]],
|
||||
progress_callback: Callable[[int, int], None] | None = None,
|
||||
) -> FirmwareInfo:
|
||||
"""Flash firmware to the SiLabs device.
|
||||
|
||||
This function is meant to be used within a firmware update context.
|
||||
"""
|
||||
if not any(
|
||||
method == expected_installed_firmware_type
|
||||
for method, _ in application_probe_methods
|
||||
):
|
||||
raise ValueError(
|
||||
f"Expected installed firmware type {expected_installed_firmware_type!r}"
|
||||
f" not in application probe methods {application_probe_methods!r}"
|
||||
)
|
||||
|
||||
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
|
||||
|
||||
flasher = flasher_cls(device=device)
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=tuple(
|
||||
(m.as_flasher_application_type(), baudrate)
|
||||
for m, baudrate in application_probe_methods
|
||||
),
|
||||
bootloader_reset=tuple(
|
||||
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
# Enter the bootloader with indeterminate progress
|
||||
@@ -400,9 +431,13 @@ async def async_flash_silabs_firmware(
|
||||
|
||||
probed_firmware_info = await probe_silabs_firmware_info(
|
||||
device,
|
||||
flasher_cls=flasher_cls,
|
||||
bootloader_reset_methods=bootloader_reset_methods,
|
||||
# Only probe for the expected installed firmware type
|
||||
application_probe_methods=[expected_installed_firmware_type],
|
||||
application_probe_methods=[
|
||||
(method, baudrate)
|
||||
for method, baudrate in application_probe_methods
|
||||
if method == expected_installed_firmware_type
|
||||
],
|
||||
)
|
||||
|
||||
if probed_firmware_info is None:
|
||||
|
||||
@@ -5,8 +5,6 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
|
||||
from universal_silabs_flasher.flasher import Zbt1Flasher
|
||||
|
||||
from homeassistant.components import usb
|
||||
from homeassistant.components.homeassistant_hardware import (
|
||||
firmware_config_flow,
|
||||
@@ -18,6 +16,7 @@ from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
ResetTarget,
|
||||
)
|
||||
from homeassistant.components.usb import (
|
||||
usb_service_info_from_device,
|
||||
@@ -82,7 +81,18 @@ class SkyConnectFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
context: ConfigFlowContext
|
||||
|
||||
ZIGBEE_BAUDRATE = 115200
|
||||
_flasher_cls = Zbt1Flasher
|
||||
# There is no hardware bootloader trigger
|
||||
BOOTLOADER_RESET_METHODS: list[ResetTarget] = []
|
||||
APPLICATION_PROBE_METHODS = [
|
||||
(ApplicationType.GECKO_BOOTLOADER, 115200),
|
||||
(ApplicationType.EZSP, ZIGBEE_BAUDRATE),
|
||||
(ApplicationType.SPINEL, 460800),
|
||||
# CPC baudrates can be removed once multiprotocol is removed
|
||||
(ApplicationType.CPC, 115200),
|
||||
(ApplicationType.CPC, 230400),
|
||||
(ApplicationType.CPC, 460800),
|
||||
(ApplicationType.ROUTER, 115200),
|
||||
]
|
||||
|
||||
def _get_translation_placeholders(self) -> dict[str, str]:
|
||||
"""Shared translation placeholders."""
|
||||
|
||||
@@ -4,8 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.flasher import Zbt1Flasher
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
@@ -25,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeAssistantSkyConnectConfigEntry
|
||||
from .config_flow import SkyConnectFirmwareMixin
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
@@ -153,7 +152,8 @@ async def async_setup_entry(
|
||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""SkyConnect firmware update entity."""
|
||||
|
||||
_flasher_cls = Zbt1Flasher
|
||||
BOOTLOADER_RESET_METHODS = SkyConnectFirmwareMixin.BOOTLOADER_RESET_METHODS
|
||||
APPLICATION_PROBE_METHODS = SkyConnectFirmwareMixin.APPLICATION_PROBE_METHODS
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -7,7 +7,6 @@ import asyncio
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Protocol, final
|
||||
|
||||
from universal_silabs_flasher.flasher import YellowFlasher
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
@@ -26,6 +25,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
ResetTarget,
|
||||
probe_silabs_firmware_info,
|
||||
)
|
||||
from homeassistant.config_entries import (
|
||||
@@ -83,7 +83,17 @@ class YellowFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
"""Mixin for Home Assistant Yellow firmware methods."""
|
||||
|
||||
ZIGBEE_BAUDRATE = 115200
|
||||
_flasher_cls = YellowFlasher
|
||||
BOOTLOADER_RESET_METHODS = [ResetTarget.YELLOW]
|
||||
APPLICATION_PROBE_METHODS = [
|
||||
(ApplicationType.GECKO_BOOTLOADER, 115200),
|
||||
(ApplicationType.EZSP, ZIGBEE_BAUDRATE),
|
||||
(ApplicationType.SPINEL, 460800),
|
||||
# CPC baudrates can be removed once multiprotocol is removed
|
||||
(ApplicationType.CPC, 115200),
|
||||
(ApplicationType.CPC, 230400),
|
||||
(ApplicationType.CPC, 460800),
|
||||
(ApplicationType.ROUTER, 115200),
|
||||
]
|
||||
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -149,7 +159,8 @@ class HomeAssistantYellowConfigFlow(
|
||||
# We do not actually use any portion of `BaseFirmwareConfigFlow` beyond this
|
||||
self._probed_firmware_info = await probe_silabs_firmware_info(
|
||||
self._device,
|
||||
flasher_cls=self._flasher_cls,
|
||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||
)
|
||||
|
||||
# Kick off ZHA hardware discovery automatically if Zigbee firmware is running
|
||||
|
||||
@@ -4,8 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.flasher import YellowFlasher
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
@@ -25,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeAssistantYellowConfigEntry
|
||||
from .config_flow import YellowFirmwareMixin
|
||||
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, MANUFACTURER, MODEL, RADIO_DEVICE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -151,7 +150,8 @@ async def async_setup_entry(
|
||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Yellow firmware update entity."""
|
||||
|
||||
_flasher_cls = YellowFlasher
|
||||
BOOTLOADER_RESET_METHODS = YellowFirmwareMixin.BOOTLOADER_RESET_METHODS
|
||||
APPLICATION_PROBE_METHODS = YellowFirmwareMixin.APPLICATION_PROBE_METHODS
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user