mirror of
https://github.com/home-assistant/core.git
synced 2026-03-24 08:18:29 +01:00
Compare commits
5 Commits
add_event_
...
add_power_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
562fcd0e34 | ||
|
|
8c39011ca4 | ||
|
|
fc0117775d | ||
|
|
3cfac4ce54 | ||
|
|
bf7f9621bf |
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.
|
||||
|
||||
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
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1309,6 +1309,8 @@ 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
|
||||
|
||||
@@ -241,13 +241,13 @@ DEFAULT_INTEGRATIONS = {
|
||||
*BASE_PLATFORMS,
|
||||
#
|
||||
# Integrations providing triggers and conditions for base platforms:
|
||||
"air_quality",
|
||||
"door",
|
||||
"garage_door",
|
||||
"gate",
|
||||
"humidity",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"power",
|
||||
"temperature",
|
||||
"window",
|
||||
}
|
||||
|
||||
@@ -3,103 +3,5 @@
|
||||
"_": {
|
||||
"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,626 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted entities to trigger on.",
|
||||
"trigger_behavior_name": "Behavior",
|
||||
"trigger_changed_above_name": "Above",
|
||||
"trigger_changed_below_name": "Below",
|
||||
"trigger_threshold_lower_limit_description": "The lower limit of the threshold.",
|
||||
"trigger_threshold_lower_limit_name": "Lower limit",
|
||||
"trigger_threshold_type_description": "The type of threshold to use.",
|
||||
"trigger_threshold_type_name": "Threshold type",
|
||||
"trigger_threshold_upper_limit_description": "The upper limit of the threshold.",
|
||||
"trigger_threshold_upper_limit_name": "Upper limit",
|
||||
"trigger_unit_description": "All values will be converted to this unit when evaluating the trigger.",
|
||||
"trigger_unit_name": "Unit of measurement"
|
||||
},
|
||||
"selector": {
|
||||
"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,241 +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,
|
||||
)
|
||||
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."""
|
||||
|
||||
class DetectedTrigger(EntityTargetStateTriggerBase):
|
||||
_domain_specs = {BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}
|
||||
_to_states = {STATE_ON}
|
||||
|
||||
return DetectedTrigger
|
||||
|
||||
|
||||
def _make_cleared_trigger(
|
||||
device_class: BinarySensorDeviceClass,
|
||||
) -> type[EntityTargetStateTriggerBase]:
|
||||
"""Create a cleared trigger for a binary sensor device class."""
|
||||
|
||||
class ClearedTrigger(EntityTargetStateTriggerBase):
|
||||
_domain_specs = {BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}
|
||||
_to_states = {STATE_OFF}
|
||||
|
||||
return ClearedTrigger
|
||||
|
||||
|
||||
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
|
||||
@@ -662,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,
|
||||
@@ -1505,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.
|
||||
|
||||
@@ -143,7 +143,6 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
}
|
||||
|
||||
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"air_quality",
|
||||
"alarm_control_panel",
|
||||
"assist_satellite",
|
||||
"button",
|
||||
@@ -151,7 +150,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"door",
|
||||
"event",
|
||||
"fan",
|
||||
"garage_door",
|
||||
"gate",
|
||||
@@ -164,6 +162,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"motion",
|
||||
"occupancy",
|
||||
"person",
|
||||
"power",
|
||||
"remote",
|
||||
"scene",
|
||||
"schedule",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260312.1"]
|
||||
"requirements": ["home-assistant-frontend==20260312.0"]
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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] = {}
|
||||
|
||||
@@ -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.2",
|
||||
"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,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"title": "Humidity",
|
||||
"triggers": {
|
||||
"changed": {
|
||||
"description": "Triggers after one or more relative humidity values change.",
|
||||
"description": "Triggers when the relative humidity changes.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Only trigger when relative humidity is above this value.",
|
||||
@@ -43,7 +43,7 @@
|
||||
"name": "Relative humidity changed"
|
||||
},
|
||||
"crossed_threshold": {
|
||||
"description": "Triggers after one or more relative humidity values cross a threshold.",
|
||||
"description": "Triggers when the relative humidity crosses a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::humidity::common::trigger_behavior_description%]",
|
||||
|
||||
@@ -39,11 +39,9 @@ HUMIDITY_DOMAIN_SPECS: dict[str, NumericalDomainSpec] = {
|
||||
}
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"changed": make_entity_numerical_state_changed_trigger(
|
||||
HUMIDITY_DOMAIN_SPECS, valid_unit="%"
|
||||
),
|
||||
"changed": make_entity_numerical_state_changed_trigger(HUMIDITY_DOMAIN_SPECS),
|
||||
"crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
HUMIDITY_DOMAIN_SPECS, valid_unit="%"
|
||||
HUMIDITY_DOMAIN_SPECS
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,10 @@
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: number
|
||||
device_class: humidity
|
||||
- domain: sensor
|
||||
device_class: humidity
|
||||
domain:
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyliebherrhomeapi"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["pyliebherrhomeapi==0.4.1"],
|
||||
"requirements": ["pyliebherrhomeapi==0.4.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "liebherr*",
|
||||
|
||||
@@ -30,10 +30,10 @@ BRIGHTNESS_DOMAIN_SPECS = {
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"brightness_changed": make_entity_numerical_state_changed_trigger(
|
||||
BRIGHTNESS_DOMAIN_SPECS, valid_unit="%"
|
||||
BRIGHTNESS_DOMAIN_SPECS
|
||||
),
|
||||
"brightness_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
BRIGHTNESS_DOMAIN_SPECS, valid_unit="%"
|
||||
BRIGHTNESS_DOMAIN_SPECS
|
||||
),
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
|
||||
@@ -30,12 +30,10 @@
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
unit_of_measurement: "%"
|
||||
domain:
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
translation_key: number_or_entity
|
||||
|
||||
turned_on: *trigger_common
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
},
|
||||
"services": {
|
||||
"set_absolute_position": {
|
||||
"description": "Sets the absolute position of a cover.",
|
||||
"description": "Sets the absolute position of the cover.",
|
||||
"fields": {
|
||||
"absolute_position": {
|
||||
"description": "Absolute position to move to.",
|
||||
|
||||
@@ -185,7 +185,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
),
|
||||
"V_PH": SensorEntityDescription(
|
||||
key="V_PH",
|
||||
device_class=SensorDeviceClass.PH,
|
||||
native_unit_of_measurement="pH",
|
||||
),
|
||||
"V_ORP": SensorEntityDescription(
|
||||
key="V_ORP",
|
||||
|
||||
@@ -126,8 +126,8 @@
|
||||
},
|
||||
"issues": {
|
||||
"return_to_grid_migration": {
|
||||
"description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those into separate export statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Energy exported to grid** section:\n\n{target_ids}\n\nOnce you have added them, ignore this issue.",
|
||||
"title": "Export to grid statistics for account: {utility_account_id}"
|
||||
"description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}\n\nOnce you have added them, ignore this issue.",
|
||||
"title": "Return to grid statistics for account: {utility_account_id}"
|
||||
},
|
||||
"unsupported_utility": {
|
||||
"fix_flow": {
|
||||
|
||||
17
homeassistant/components/power/__init__.py
Normal file
17
homeassistant/components/power/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Integration for power triggers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "power"
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
return True
|
||||
10
homeassistant/components/power/icons.json
Normal file
10
homeassistant/components/power/icons.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"triggers": {
|
||||
"changed": {
|
||||
"trigger": "mdi:flash"
|
||||
},
|
||||
"crossed_threshold": {
|
||||
"trigger": "mdi:flash"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
homeassistant/components/power/manifest.json
Normal file
8
homeassistant/components/power/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "power",
|
||||
"name": "Power",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/power",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
76
homeassistant/components/power/strings.json
Normal file
76
homeassistant/components/power/strings.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted entities to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"selector": {
|
||||
"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": "Power",
|
||||
"triggers": {
|
||||
"changed": {
|
||||
"description": "Triggers after one or more power values change.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Only trigger when power is above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when power 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": "Power changed"
|
||||
},
|
||||
"crossed_threshold": {
|
||||
"description": "Triggers after one or more power values cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::power::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::power::common::trigger_behavior_name%]"
|
||||
},
|
||||
"lower_limit": {
|
||||
"description": "The lower limit of the threshold.",
|
||||
"name": "Lower limit"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "The type of threshold to use.",
|
||||
"name": "Threshold type"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::power::triggers::changed::fields::unit::description%]",
|
||||
"name": "[%key:component::power::triggers::changed::fields::unit::name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "The upper limit of the threshold.",
|
||||
"name": "Upper limit"
|
||||
}
|
||||
},
|
||||
"name": "Power crossed threshold"
|
||||
}
|
||||
}
|
||||
}
|
||||
52
homeassistant/components/power/trigger.py
Normal file
52
homeassistant/components/power/trigger.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Provides triggers for power."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import NumericalDomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
|
||||
EntityNumericalStateTriggerWithUnitBase,
|
||||
Trigger,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import PowerConverter
|
||||
|
||||
POWER_DOMAIN_SPECS: dict[str, NumericalDomainSpec] = {
|
||||
NUMBER_DOMAIN: NumericalDomainSpec(device_class=NumberDeviceClass.POWER),
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.POWER),
|
||||
}
|
||||
|
||||
|
||||
class _PowerTriggerMixin(EntityNumericalStateTriggerWithUnitBase):
|
||||
"""Mixin for power triggers providing entity filtering, value extraction, and unit conversion."""
|
||||
|
||||
_base_unit = UnitOfPower.WATT
|
||||
_domain_specs = POWER_DOMAIN_SPECS
|
||||
_unit_converter = PowerConverter
|
||||
|
||||
|
||||
class PowerChangedTrigger(
|
||||
_PowerTriggerMixin, EntityNumericalStateChangedTriggerWithUnitBase
|
||||
):
|
||||
"""Trigger for power value changes."""
|
||||
|
||||
|
||||
class PowerCrossedThresholdTrigger(
|
||||
_PowerTriggerMixin, EntityNumericalStateCrossedThresholdTriggerWithUnitBase
|
||||
):
|
||||
"""Trigger for power value crossing a threshold."""
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"changed": PowerChangedTrigger,
|
||||
"crossed_threshold": PowerCrossedThresholdTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for power."""
|
||||
return TRIGGERS
|
||||
87
homeassistant/components/power/triggers.yaml
Normal file
87
homeassistant/components/power/triggers.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
.trigger_common_fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "mW"
|
||||
- "W"
|
||||
- "kW"
|
||||
- "MW"
|
||||
- "GW"
|
||||
- "TW"
|
||||
- "BTU/h"
|
||||
- domain: sensor
|
||||
device_class: power
|
||||
- domain: number
|
||||
device_class: power
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- above
|
||||
- below
|
||||
- between
|
||||
- outside
|
||||
translation_key: trigger_threshold_type
|
||||
|
||||
.trigger_unit: &trigger_unit
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "mW"
|
||||
- "W"
|
||||
- "kW"
|
||||
- "MW"
|
||||
- "GW"
|
||||
- "TW"
|
||||
- "BTU/h"
|
||||
|
||||
.trigger_target: &trigger_target
|
||||
entity:
|
||||
- domain: number
|
||||
device_class: power
|
||||
- domain: sensor
|
||||
device_class: power
|
||||
|
||||
changed:
|
||||
target: *trigger_target
|
||||
fields:
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
unit: *trigger_unit
|
||||
|
||||
crossed_threshold:
|
||||
target: *trigger_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity
|
||||
upper_limit: *number_or_entity
|
||||
unit: *trigger_unit
|
||||
@@ -1,18 +1,14 @@
|
||||
"""Provides triggers for switch platform."""
|
||||
|
||||
from homeassistant.components.input_boolean import DOMAIN as INPUT_BOOLEAN_DOMAIN
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
SWITCH_DOMAIN_SPECS = {DOMAIN: DomainSpec(), INPUT_BOOLEAN_DOMAIN: DomainSpec()}
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turned_on": make_entity_target_state_trigger(SWITCH_DOMAIN_SPECS, STATE_ON),
|
||||
"turned_off": make_entity_target_state_trigger(SWITCH_DOMAIN_SPECS, STATE_OFF),
|
||||
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
- domain: switch
|
||||
- domain: input_boolean
|
||||
domain: switch
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
|
||||
@@ -153,7 +153,6 @@ STEP_WEBHOOKS_DATA_SCHEMA: vol.Schema = vol.Schema(
|
||||
vol.Required(CONF_TRUSTED_NETWORKS): vol.Coerce(str),
|
||||
}
|
||||
)
|
||||
SUBENTRY_SCHEMA: vol.Schema = vol.Schema({vol.Required(CONF_CHAT_ID): vol.Coerce(int)})
|
||||
OPTIONS_SCHEMA: vol.Schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
@@ -599,30 +598,24 @@ class AllowedChatIdsSubEntryFlowHandler(ConfigSubentryFlow):
|
||||
)
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders = DESCRIPTION_PLACEHOLDERS.copy()
|
||||
|
||||
if user_input is not None:
|
||||
config_entry: TelegramBotConfigEntry = self._get_entry()
|
||||
bot = config_entry.runtime_data.bot
|
||||
|
||||
# validate chat id
|
||||
chat_id: int = user_input[CONF_CHAT_ID]
|
||||
try:
|
||||
chat_info: ChatFullInfo = await bot.get_chat(chat_id)
|
||||
except BadRequest:
|
||||
errors["base"] = "chat_not_found"
|
||||
except TelegramError as err:
|
||||
errors["base"] = "telegram_error"
|
||||
description_placeholders[ERROR_MESSAGE] = str(err)
|
||||
|
||||
if not errors:
|
||||
chat_name = await _async_get_chat_name(bot, chat_id)
|
||||
if chat_name:
|
||||
return self.async_create_entry(
|
||||
title=chat_info.effective_name or str(chat_id),
|
||||
title=f"{chat_name} ({chat_id})",
|
||||
data={CONF_CHAT_ID: chat_id},
|
||||
unique_id=str(chat_id),
|
||||
)
|
||||
|
||||
errors["base"] = "chat_not_found"
|
||||
|
||||
service: TelegramNotificationService = self._get_entry().runtime_data
|
||||
description_placeholders = DESCRIPTION_PLACEHOLDERS.copy()
|
||||
description_placeholders["bot_username"] = f"@{service.bot.username}"
|
||||
description_placeholders["bot_url"] = f"https://t.me/{service.bot.username}"
|
||||
|
||||
@@ -646,7 +639,7 @@ class AllowedChatIdsSubEntryFlowHandler(ConfigSubentryFlow):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
SUBENTRY_SCHEMA,
|
||||
vol.Schema({vol.Required(CONF_CHAT_ID): vol.Coerce(int)}),
|
||||
suggested_values,
|
||||
),
|
||||
description_placeholders=description_placeholders,
|
||||
@@ -684,3 +677,11 @@ async def _get_most_recent_chat(
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def _async_get_chat_name(bot: Bot, chat_id: int) -> str:
|
||||
try:
|
||||
chat_info: ChatFullInfo = await bot.get_chat(chat_id)
|
||||
return chat_info.effective_name or str(chat_id)
|
||||
except BadRequest:
|
||||
return ""
|
||||
|
||||
@@ -92,8 +92,7 @@
|
||||
},
|
||||
"entry_type": "Allowed chat ID",
|
||||
"error": {
|
||||
"chat_not_found": "Chat not found",
|
||||
"telegram_error": "[%key:component::telegram_bot::config::error::telegram_error%]"
|
||||
"chat_not_found": "Chat not found"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "Add allowed chat ID"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"title": "Temperature",
|
||||
"triggers": {
|
||||
"changed": {
|
||||
"description": "Triggers after one or more temperatures change.",
|
||||
"description": "Triggers when the temperature changes.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Only trigger when temperature is above this value.",
|
||||
@@ -47,7 +47,7 @@
|
||||
"name": "Temperature changed"
|
||||
},
|
||||
"crossed_threshold": {
|
||||
"description": "Triggers after one or more temperatures cross a threshold.",
|
||||
"description": "Triggers when the temperature crosses a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::temperature::common::trigger_behavior_description%]",
|
||||
|
||||
@@ -80,16 +80,6 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_ARM_AWAY_ACTION,
|
||||
CONF_ARM_CUSTOM_BYPASS_ACTION,
|
||||
CONF_ARM_HOME_ACTION,
|
||||
CONF_ARM_NIGHT_ACTION,
|
||||
CONF_ARM_VACATION_ACTION,
|
||||
CONF_DISARM_ACTION,
|
||||
CONF_TRIGGER_ACTION,
|
||||
)
|
||||
|
||||
DEFAULT_NAME = "Template Alarm Control Panel"
|
||||
|
||||
ALARM_CONTROL_PANEL_COMMON_SCHEMA = vol.Schema(
|
||||
@@ -162,7 +152,6 @@ async def async_setup_entry(
|
||||
StateAlarmControlPanelEntity,
|
||||
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -183,7 +172,6 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_ALARM_CONTROL_PANELS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -209,7 +197,6 @@ class AbstractTemplateAlarmControlPanel(
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity calls AbstractTemplateEntity.__init__.
|
||||
def __init__(self, name: str) -> None: # pylint: disable=super-init-not-called
|
||||
@@ -219,6 +206,7 @@ class AbstractTemplateAlarmControlPanel(
|
||||
self._attr_code_format = self._config[CONF_CODE_FORMAT].value
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_alarm_state",
|
||||
validator=tcv.strenum(self, CONF_STATE, AlarmControlPanelState),
|
||||
)
|
||||
|
||||
@@ -176,7 +176,6 @@ class AbstractTemplateBinarySensor(
|
||||
"""Representation of a template binary sensor features."""
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -190,6 +189,7 @@ class AbstractTemplateBinarySensor(
|
||||
self._delay_cancel: CALLBACK_TYPE | None = None
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_is_on",
|
||||
on_update=self._update_state,
|
||||
)
|
||||
|
||||
@@ -36,8 +36,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_NAME = "Template Button"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
SCRIPT_FIELDS = (CONF_PRESS,)
|
||||
|
||||
BUTTON_YAML_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
|
||||
@@ -68,7 +66,6 @@ async def async_setup_platform(
|
||||
None,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -84,7 +81,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateButtonEntity,
|
||||
BUTTON_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -71,14 +71,6 @@ CONF_TILT_OPTIMISTIC = "tilt_optimistic"
|
||||
|
||||
CONF_OPEN_AND_CLOSE = "open_or_close"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CLOSE_ACTION,
|
||||
OPEN_ACTION,
|
||||
POSITION_ACTION,
|
||||
STOP_ACTION,
|
||||
TILT_ACTION,
|
||||
)
|
||||
|
||||
TILT_FEATURES = (
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
@@ -173,7 +165,6 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_COVERS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -190,7 +181,6 @@ async def async_setup_entry(
|
||||
StateCoverEntity,
|
||||
COVER_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -215,7 +205,6 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity):
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_extra_optimistic_options = (CONF_POSITION,)
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -223,6 +212,7 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity):
|
||||
"""Initialize the features."""
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_current_cover_position",
|
||||
template_validators.strenum(
|
||||
self, CONF_STATE, CoverState, CoverState.OPEN, CoverState.CLOSED
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_OPTIMISTIC
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_OPTIMISTIC, CONF_STATE
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||
@@ -15,6 +16,8 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_DEFAULT_ENTITY_ID
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityTemplate:
|
||||
@@ -33,7 +36,7 @@ class AbstractTemplateEntity(Entity):
|
||||
_entity_id_format: str
|
||||
_optimistic_entity: bool = False
|
||||
_extra_optimistic_options: tuple[str, ...] | None = None
|
||||
_state_option: str | None = None
|
||||
_template: Template | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -50,18 +53,19 @@ class AbstractTemplateEntity(Entity):
|
||||
if self._optimistic_entity:
|
||||
optimistic = config.get(CONF_OPTIMISTIC)
|
||||
|
||||
if self._state_option is not None:
|
||||
assumed_optimistic = config.get(self._state_option) is None
|
||||
if self._extra_optimistic_options:
|
||||
assumed_optimistic = assumed_optimistic and all(
|
||||
config.get(option) is None
|
||||
for option in self._extra_optimistic_options
|
||||
)
|
||||
self._template = config.get(CONF_STATE)
|
||||
|
||||
self._attr_assumed_state = optimistic or (
|
||||
optimistic is None and assumed_optimistic
|
||||
assumed_optimistic = self._template is None
|
||||
if self._extra_optimistic_options:
|
||||
assumed_optimistic = assumed_optimistic and all(
|
||||
config.get(option) is None
|
||||
for option in self._extra_optimistic_options
|
||||
)
|
||||
|
||||
self._attr_assumed_state = optimistic or (
|
||||
optimistic is None and assumed_optimistic
|
||||
)
|
||||
|
||||
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
||||
_, _, object_id = default_entity_id.partition(".")
|
||||
self.entity_id = async_generate_entity_id(
|
||||
@@ -85,16 +89,12 @@ class AbstractTemplateEntity(Entity):
|
||||
@abstractmethod
|
||||
def setup_state_template(
|
||||
self,
|
||||
option: str,
|
||||
attribute: str,
|
||||
validator: Callable[[Any], Any] | None = None,
|
||||
on_update: Callable[[Any], None] | None = None,
|
||||
) -> None:
|
||||
"""Set up a template that manages the main state of the entity.
|
||||
|
||||
Requires _state_option to be set on the inheriting class. _state_option represents
|
||||
the configuration option that derives the state. E.g. Template weather entities main state option
|
||||
is 'condition', where switch is 'state'.
|
||||
"""
|
||||
"""Set up a template that manages the main state of the entity."""
|
||||
|
||||
@abstractmethod
|
||||
def setup_template(
|
||||
|
||||
@@ -87,15 +87,6 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Fan"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_OFF_ACTION,
|
||||
CONF_ON_ACTION,
|
||||
CONF_SET_DIRECTION_ACTION,
|
||||
CONF_SET_OSCILLATING_ACTION,
|
||||
CONF_SET_PERCENTAGE_ACTION,
|
||||
CONF_SET_PRESET_MODE_ACTION,
|
||||
)
|
||||
|
||||
FAN_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DIRECTION): cv.template,
|
||||
@@ -168,7 +159,6 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_FANS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -184,7 +174,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateFanEntity,
|
||||
FAN_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -207,13 +196,13 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
def __init__(self, name: str, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called
|
||||
"""Initialize the features."""
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_is_on",
|
||||
template_validators.boolean(self, CONF_STATE),
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.components import blueprint
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -26,7 +25,7 @@ from homeassistant.const import (
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import issue_registry as ir, template
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
@@ -35,7 +34,6 @@ from homeassistant.helpers.entity_platform import (
|
||||
async_get_platforms,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.script import async_validate_actions_config
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -210,21 +208,6 @@ def _format_template(value: Any, field: str | None = None) -> Any:
|
||||
return str(value)
|
||||
|
||||
|
||||
def _get_config_breadcrumbs(config: ConfigType) -> str:
|
||||
"""Try to coerce entity information from the config."""
|
||||
breadcrumb = "Template Entity"
|
||||
# Default entity id should be in most legacy configuration because
|
||||
# it's created from the legacy slug. Vacuum and Lock do not have a
|
||||
# slug, therefore we need to use the name or unique_id.
|
||||
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
||||
breadcrumb = default_entity_id.split(".")[-1]
|
||||
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
|
||||
breadcrumb = f"unique_id: {unique_id}"
|
||||
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
|
||||
breadcrumb = name.template
|
||||
return breadcrumb
|
||||
|
||||
|
||||
def format_migration_config(
|
||||
config: ConfigType | list[ConfigType], depth: int = 0
|
||||
) -> ConfigType | list[ConfigType]:
|
||||
@@ -269,7 +252,16 @@ def create_legacy_template_issue(
|
||||
if domain not in PLATFORMS:
|
||||
return
|
||||
|
||||
breadcrumb = _get_config_breadcrumbs(config)
|
||||
breadcrumb = "Template Entity"
|
||||
# Default entity id should be in most legacy configuration because
|
||||
# it's created from the legacy slug. Vacuum and Lock do not have a
|
||||
# slug, therefore we need to use the name or unique_id.
|
||||
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
||||
breadcrumb = default_entity_id.split(".")[-1]
|
||||
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
|
||||
breadcrumb = f"unique_id: {unique_id}"
|
||||
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
|
||||
breadcrumb = name.template
|
||||
|
||||
issue_id = f"{LEGACY_TEMPLATE_DEPRECATION_KEY}_{domain}_{breadcrumb}_{hashlib.md5(','.join(config.keys()).encode()).hexdigest()}"
|
||||
|
||||
@@ -304,39 +296,6 @@ def create_legacy_template_issue(
|
||||
)
|
||||
|
||||
|
||||
async def validate_template_scripts(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Validate template scripts."""
|
||||
if not script_options:
|
||||
return
|
||||
|
||||
def _humanize(err: Exception, data: Any) -> str:
|
||||
"""Humanize vol.Invalid, stringify other exceptions."""
|
||||
if isinstance(err, vol.Invalid):
|
||||
return humanize_error(data, err)
|
||||
return str(err)
|
||||
|
||||
breadcrumb: str | None = None
|
||||
for script_option in script_options:
|
||||
if (script_config := config.pop(script_option, None)) is not None:
|
||||
try:
|
||||
config[script_option] = await async_validate_actions_config(
|
||||
hass, script_config
|
||||
)
|
||||
except (vol.Invalid, HomeAssistantError) as err:
|
||||
if not breadcrumb:
|
||||
breadcrumb = _get_config_breadcrumbs(config)
|
||||
_LOGGER.error(
|
||||
"The '%s' actions for %s failed to setup: %s",
|
||||
script_option,
|
||||
breadcrumb,
|
||||
_humanize(err, script_config),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_template_platform(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
@@ -347,7 +306,6 @@ async def async_setup_template_platform(
|
||||
discovery_info: DiscoveryInfoType | None,
|
||||
legacy_fields: dict[str, str] | None = None,
|
||||
legacy_key: str | None = None,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Set up the Template platform."""
|
||||
if discovery_info is None:
|
||||
@@ -379,14 +337,10 @@ async def async_setup_template_platform(
|
||||
# Trigger Configuration
|
||||
if "coordinator" in discovery_info:
|
||||
if trigger_entity_cls:
|
||||
entities = []
|
||||
for entity_config in discovery_info["entities"]:
|
||||
await validate_template_scripts(hass, entity_config, script_options)
|
||||
entities.append(
|
||||
trigger_entity_cls(
|
||||
hass, discovery_info["coordinator"], entity_config
|
||||
)
|
||||
)
|
||||
entities = [
|
||||
trigger_entity_cls(hass, discovery_info["coordinator"], config)
|
||||
for config in discovery_info["entities"]
|
||||
]
|
||||
async_add_entities(entities)
|
||||
else:
|
||||
raise PlatformNotReady(
|
||||
@@ -395,9 +349,6 @@ async def async_setup_template_platform(
|
||||
return
|
||||
|
||||
# Modern Configuration
|
||||
for entity_config in discovery_info["entities"]:
|
||||
await validate_template_scripts(hass, entity_config, script_options)
|
||||
|
||||
async_create_template_tracking_entities(
|
||||
state_entity_cls,
|
||||
async_add_entities,
|
||||
@@ -414,7 +365,6 @@ async def async_setup_template_entry(
|
||||
state_entity_cls: type[TemplateEntity],
|
||||
config_schema: vol.Schema | vol.All,
|
||||
replace_value_template: bool = False,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Setup the Template from a config entry."""
|
||||
options = dict(config_entry.options)
|
||||
@@ -427,7 +377,6 @@ async def async_setup_template_entry(
|
||||
options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE)
|
||||
|
||||
validated_config = config_schema(options)
|
||||
await validate_template_scripts(hass, validated_config, script_options)
|
||||
|
||||
async_add_entities(
|
||||
[state_entity_cls(hass, validated_config, config_entry.entry_id)]
|
||||
|
||||
@@ -129,18 +129,6 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Light"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_EFFECT_ACTION,
|
||||
CONF_HS_ACTION,
|
||||
CONF_LEVEL_ACTION,
|
||||
CONF_OFF_ACTION,
|
||||
CONF_ON_ACTION,
|
||||
CONF_RGB_ACTION,
|
||||
CONF_RGBW_ACTION,
|
||||
CONF_RGBWW_ACTION,
|
||||
CONF_TEMPERATURE_ACTION,
|
||||
)
|
||||
|
||||
LIGHT_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
|
||||
@@ -154,6 +142,8 @@ LIGHT_COMMON_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_MIN_MIREDS): cv.template,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGB): cv.template,
|
||||
vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA,
|
||||
@@ -236,7 +226,6 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_LIGHTS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -253,7 +242,6 @@ async def async_setup_entry(
|
||||
StateLightEntity,
|
||||
LIGHT_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -359,7 +347,6 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
|
||||
_optimistic_entity = True
|
||||
_attr_max_color_temp_kelvin = DEFAULT_MAX_KELVIN
|
||||
_attr_min_color_temp_kelvin = DEFAULT_MIN_KELVIN
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -370,7 +357,7 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
|
||||
|
||||
# Setup state and brightness
|
||||
self.setup_state_template(
|
||||
"_attr_is_on", template_validators.boolean(self, CONF_STATE)
|
||||
CONF_STATE, "_attr_is_on", template_validators.boolean(self, CONF_STATE)
|
||||
)
|
||||
self.setup_template(
|
||||
CONF_LEVEL,
|
||||
|
||||
@@ -64,13 +64,6 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_LOCK,
|
||||
CONF_OPEN,
|
||||
CONF_UNLOCK,
|
||||
)
|
||||
|
||||
|
||||
LOCK_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CODE_FORMAT): cv.template,
|
||||
@@ -119,7 +112,6 @@ async def async_setup_platform(
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -135,7 +127,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateLockEntity,
|
||||
LOCK_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -158,7 +149,6 @@ class AbstractTemplateLock(AbstractTemplateEntity, LockEntity):
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -167,6 +157,7 @@ class AbstractTemplateLock(AbstractTemplateEntity, LockEntity):
|
||||
self._code_format_template_error: TemplateError | None = None
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_lock_state",
|
||||
template_validators.strenum(
|
||||
self, CONF_STATE, LockState, LockState.LOCKED, LockState.UNLOCKED
|
||||
@@ -192,18 +183,16 @@ class AbstractTemplateLock(AbstractTemplateEntity, LockEntity):
|
||||
self._attr_supported_features |= supported_feature
|
||||
|
||||
def _set_state(self, state: LockState | None) -> None:
|
||||
if state is None:
|
||||
self._attr_is_locked = None
|
||||
return
|
||||
|
||||
self._attr_is_jammed = state == LockState.JAMMED
|
||||
self._attr_is_opening = state == LockState.OPENING
|
||||
self._attr_is_locking = state == LockState.LOCKING
|
||||
self._attr_is_open = state == LockState.OPEN
|
||||
self._attr_is_unlocking = state == LockState.UNLOCKING
|
||||
|
||||
# All other parameters need to be set False in order
|
||||
# for the lock to be unknown.
|
||||
if state is None:
|
||||
self._attr_is_locked = state
|
||||
else:
|
||||
self._attr_is_locked = state == LockState.LOCKED
|
||||
self._attr_is_locked = state == LockState.LOCKED
|
||||
|
||||
@callback
|
||||
def _update_code_format(self, render: str | TemplateError | None):
|
||||
|
||||
@@ -46,8 +46,6 @@ CONF_SET_VALUE = "set_value"
|
||||
DEFAULT_NAME = "Template Number"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
SCRIPT_FIELDS = (CONF_SET_VALUE,)
|
||||
|
||||
NUMBER_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
|
||||
@@ -83,7 +81,6 @@ async def async_setup_platform(
|
||||
TriggerNumberEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -99,7 +96,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateNumberEntity,
|
||||
NUMBER_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -118,7 +114,6 @@ class AbstractTemplateNumber(AbstractTemplateEntity, NumberEntity):
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -130,6 +125,7 @@ class AbstractTemplateNumber(AbstractTemplateEntity, NumberEntity):
|
||||
self._attr_native_max_value = DEFAULT_MAX_VALUE
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_native_value",
|
||||
template_validators.number(self, CONF_STATE),
|
||||
)
|
||||
|
||||
@@ -47,8 +47,6 @@ CONF_SELECT_OPTION = "select_option"
|
||||
|
||||
DEFAULT_NAME = "Template Select"
|
||||
|
||||
SCRIPT_FIELDS = (CONF_SELECT_OPTION,)
|
||||
|
||||
SELECT_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_OPTIONS): cv.template,
|
||||
@@ -81,7 +79,6 @@ async def async_setup_platform(
|
||||
TriggerSelectEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -97,7 +94,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
TemplateSelect,
|
||||
SELECT_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -116,7 +112,6 @@ class AbstractTemplateSelect(AbstractTemplateEntity, SelectEntity):
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -125,6 +120,7 @@ class AbstractTemplateSelect(AbstractTemplateEntity, SelectEntity):
|
||||
self._attr_options = []
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_current_option",
|
||||
cv.string,
|
||||
)
|
||||
|
||||
@@ -229,7 +229,6 @@ class AbstractTemplateSensor(AbstractTemplateEntity, RestoreSensor):
|
||||
"""Representation of a template sensor features."""
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -241,6 +240,7 @@ class AbstractTemplateSensor(AbstractTemplateEntity, RestoreSensor):
|
||||
self._attr_last_reset = None
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_native_value",
|
||||
self._validate_state,
|
||||
)
|
||||
|
||||
@@ -57,16 +57,11 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Switch"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_TURN_OFF,
|
||||
CONF_TURN_ON,
|
||||
)
|
||||
|
||||
SWITCH_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,7 +109,6 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_SWITCHES,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -131,7 +125,6 @@ async def async_setup_entry(
|
||||
StateSwitchEntity,
|
||||
SWITCH_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -155,7 +148,6 @@ class AbstractTemplateSwitch(AbstractTemplateEntity, SwitchEntity, RestoreEntity
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -163,6 +155,7 @@ class AbstractTemplateSwitch(AbstractTemplateEntity, SwitchEntity, RestoreEntity
|
||||
"""Initialize the features."""
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_is_on",
|
||||
template_validators.boolean(self, CONF_STATE),
|
||||
)
|
||||
|
||||
@@ -292,16 +292,12 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
|
||||
def setup_state_template(
|
||||
self,
|
||||
option: str,
|
||||
attribute: str,
|
||||
validator: Callable[[Any], Any] | None = None,
|
||||
on_update: Callable[[Any], None] | None = None,
|
||||
) -> None:
|
||||
"""Set up a template that manages the main state of the entity.
|
||||
|
||||
Requires _state_option to be set on the inheriting class. _state_option represents
|
||||
the configuration option that derives the state. E.g. Template weather entities main state option
|
||||
is 'condition', where switch is 'state'.
|
||||
"""
|
||||
"""Set up a template that manages the main state of the entity."""
|
||||
|
||||
@callback
|
||||
def _update_state(result: Any) -> None:
|
||||
@@ -318,22 +314,13 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
self._attr_available = True
|
||||
|
||||
state = validator(result) if validator else result
|
||||
|
||||
if on_update:
|
||||
on_update(state)
|
||||
else:
|
||||
setattr(self, attribute, state)
|
||||
|
||||
if self._state_option is None:
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not implement '_state_option' for 'setup_state_template'."
|
||||
)
|
||||
|
||||
self.add_template(
|
||||
self._state_option,
|
||||
attribute,
|
||||
on_update=_update_state,
|
||||
none_on_template_error=False,
|
||||
option, attribute, on_update=_update_state, none_on_template_error=False
|
||||
)
|
||||
|
||||
def setup_template(
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import CONF_VARIABLES
|
||||
from homeassistant.const import CONF_STATE, CONF_VARIABLES
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
@@ -60,30 +60,17 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
|
||||
def setup_state_template(
|
||||
self,
|
||||
option: str,
|
||||
attribute: str,
|
||||
validator: Callable[[Any], Any] | None = None,
|
||||
on_update: Callable[[Any], None] | None = None,
|
||||
) -> None:
|
||||
"""Set up a template that manages the main state of the entity.
|
||||
|
||||
Requires _state_option to be set on the inheriting class. _state_option represents
|
||||
the configuration option that derives the state. E.g. Template weather entities main state option
|
||||
is 'condition', where switch is 'state'.
|
||||
"""
|
||||
if self._state_option is None:
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not implement '_state_option' for 'setup_state_template'."
|
||||
)
|
||||
|
||||
"""Set up a template that manages the main state of the entity."""
|
||||
if self.add_template(
|
||||
self._state_option,
|
||||
attribute,
|
||||
validator,
|
||||
on_update,
|
||||
none_on_template_error=False,
|
||||
option, attribute, validator, on_update, none_on_template_error=False
|
||||
):
|
||||
self._to_render_simple.append(self._state_option)
|
||||
self._parse_result.add(self._state_option)
|
||||
self._to_render_simple.append(option)
|
||||
self._parse_result.add(option)
|
||||
|
||||
def setup_template(
|
||||
self,
|
||||
@@ -162,7 +149,7 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
# Filter out state templates because they have unique behavior
|
||||
# with none_on_template_error.
|
||||
if (
|
||||
key != self._state_option
|
||||
key != CONF_STATE
|
||||
and key in self._templates
|
||||
and not self._templates[key].none_on_template_error
|
||||
):
|
||||
@@ -177,21 +164,17 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
|
||||
# If state fails to render, the entity should go unavailable. Render the
|
||||
# state as a simple template because the result should always be a string or None.
|
||||
if (
|
||||
state_option := self._state_option
|
||||
) is not None and state_option in self._to_render_simple:
|
||||
if CONF_STATE in self._to_render_simple:
|
||||
if (
|
||||
result := self._render_single_template(state_option, variables)
|
||||
result := self._render_single_template(CONF_STATE, variables)
|
||||
) is _SENTINEL:
|
||||
self._rendered = self._static_rendered
|
||||
self._state_render_error = True
|
||||
return
|
||||
|
||||
rendered[state_option] = result
|
||||
rendered[CONF_STATE] = result
|
||||
|
||||
self._render_single_templates(
|
||||
rendered, variables, [state_option] if state_option else []
|
||||
)
|
||||
self._render_single_templates(rendered, variables, [CONF_STATE])
|
||||
self._render_attributes(rendered, variables)
|
||||
self._rendered = rendered
|
||||
|
||||
@@ -199,10 +182,6 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
"""Get a rendered result and return the value."""
|
||||
# Handle any templates.
|
||||
write_state = False
|
||||
if self._state_render_error:
|
||||
# The state errored and the entity is unavailable, do not process any values.
|
||||
return True
|
||||
|
||||
for option, entity_template in self._templates.items():
|
||||
# Capture templates that did not render a result due to an exception and
|
||||
# ensure the state object updates. _SENTINEL is used to differentiate
|
||||
@@ -246,7 +225,18 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
if self._render_availability_template(variables):
|
||||
self._render_templates(variables)
|
||||
|
||||
write_state = self._handle_rendered_results()
|
||||
write_state = False
|
||||
# While transitioning platforms to the new framework, this
|
||||
# if-statement is necessary for backward compatibility with existing
|
||||
# trigger based platforms.
|
||||
if self._templates:
|
||||
# Handle any results that were rendered.
|
||||
write_state = self._handle_rendered_results()
|
||||
|
||||
# Check availability after rendering the results because the state
|
||||
# template could render the entity unavailable
|
||||
if not self.available:
|
||||
write_state = True
|
||||
|
||||
if len(self._rendered) > 0:
|
||||
# In some cases, the entity may be state optimistic or
|
||||
|
||||
@@ -65,8 +65,6 @@ CONF_SPECIFIC_VERSION = "specific_version"
|
||||
CONF_TITLE = "title"
|
||||
CONF_UPDATE_PERCENTAGE = "update_percentage"
|
||||
|
||||
SCRIPT_FIELDS = (CONF_INSTALL,)
|
||||
|
||||
UPDATE_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BACKUP, default=False): cv.boolean,
|
||||
@@ -107,7 +105,6 @@ async def async_setup_platform(
|
||||
TriggerUpdateEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -123,7 +120,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateUpdateEntity,
|
||||
UPDATE_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -76,16 +76,6 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
SERVICE_CLEAN_SPOT,
|
||||
SERVICE_LOCATE,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_RETURN_TO_BASE,
|
||||
SERVICE_SET_FAN_SPEED,
|
||||
SERVICE_START,
|
||||
SERVICE_STOP,
|
||||
)
|
||||
|
||||
VACUUM_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BATTERY_LEVEL): cv.template,
|
||||
@@ -160,7 +150,6 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_VACUUMS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -176,7 +165,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
TemplateStateVacuumEntity,
|
||||
VACUUM_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -219,7 +207,6 @@ class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -229,6 +216,7 @@ class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
|
||||
# List of valid fan speeds
|
||||
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_activity",
|
||||
template_validators.strenum(self, CONF_STATE, VacuumActivity),
|
||||
)
|
||||
|
||||
@@ -389,7 +389,6 @@ class AbstractTemplateWeather(AbstractTemplateEntity, WeatherEntity):
|
||||
"""Representation of a template weathers features."""
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_state_option = CONF_CONDITION
|
||||
_optimistic_entity = True
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
@@ -400,7 +399,8 @@ class AbstractTemplateWeather(AbstractTemplateEntity, WeatherEntity):
|
||||
"""Initialize the features."""
|
||||
|
||||
# Required options
|
||||
self.setup_state_template(
|
||||
self.setup_template(
|
||||
CONF_CONDITION,
|
||||
"_attr_condition",
|
||||
template_validators.item_in_list(self, CONF_CONDITION, CONDITION_CLASSES),
|
||||
)
|
||||
|
||||
@@ -240,7 +240,7 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""An entity that represents a To-do list."""
|
||||
|
||||
_attr_todo_items: list[TodoItem] | None = None
|
||||
_update_listeners: list[Callable[[list[TodoItem] | None], None]] | None = None
|
||||
_update_listeners: list[Callable[[list[TodoItem]], None]] | None = None
|
||||
|
||||
@property
|
||||
def state(self) -> int | None:
|
||||
@@ -281,7 +281,7 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
@final
|
||||
@callback
|
||||
def async_subscribe_updates(
|
||||
self, listener: Callable[[list[TodoItem] | None], None]
|
||||
self, listener: Callable[[list[TodoItem]], None]
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Subscribe to To-do list item updates."""
|
||||
if self._update_listeners is None:
|
||||
@@ -302,12 +302,7 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if not self._update_listeners:
|
||||
return
|
||||
|
||||
todo_items = (
|
||||
[copy.copy(item) for item in self.todo_items]
|
||||
if self.todo_items is not None
|
||||
else None
|
||||
)
|
||||
|
||||
todo_items = [copy.copy(item) for item in self.todo_items or []]
|
||||
for listener in self._update_listeners:
|
||||
listener(todo_items)
|
||||
|
||||
@@ -340,13 +335,14 @@ async def websocket_handle_subscribe_todo_items(
|
||||
return
|
||||
|
||||
@callback
|
||||
def todo_item_listener(todo_items: list[TodoItem] | None) -> None:
|
||||
def todo_item_listener(todo_items: list[TodoItem]) -> None:
|
||||
"""Push updated To-do list items to websocket."""
|
||||
items = [dataclasses.asdict(item) for item in todo_items or []]
|
||||
connection.send_message(
|
||||
websocket_api.event_message(
|
||||
msg["id"],
|
||||
{"items": items},
|
||||
{
|
||||
"items": [dataclasses.asdict(item) for item in todo_items],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,46 +1 @@
|
||||
"""The touchline component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from pytouchline_extended import PyTouchline
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .data import TouchlineConfigEntry, TouchlineData
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: TouchlineConfigEntry) -> bool:
|
||||
"""Set up touchline from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
_LOGGER.debug(
|
||||
"Touchline entry id: %s Unique id: %s", entry.entry_id, entry.unique_id
|
||||
)
|
||||
py_touchline = PyTouchline(url=host)
|
||||
try:
|
||||
number_of_devices = int(
|
||||
await hass.async_add_executor_job(py_touchline.get_number_of_devices)
|
||||
)
|
||||
except (OSError, ConnectionError, TimeoutError) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Error while connecting to Touchline controller at {host}"
|
||||
) from err
|
||||
|
||||
entry.runtime_data = TouchlineData(
|
||||
touchline=py_touchline, number_of_devices=number_of_devices
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a touchline config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -13,20 +13,12 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_HOST, UnitOfTemperature
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .data import TouchlineConfigEntry
|
||||
|
||||
|
||||
class PresetMode(NamedTuple):
|
||||
"""Settings for preset mode."""
|
||||
@@ -52,73 +44,22 @@ TOUCHLINE_HA_PRESETS = {
|
||||
PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string})
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: TouchlineConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Touchline devices from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
|
||||
devices = [
|
||||
Touchline(PyTouchline(id=device_id, url=host))
|
||||
for device_id in range(entry.runtime_data.number_of_devices)
|
||||
]
|
||||
async_add_entities(devices, True)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Touchline devices from YAML.
|
||||
"""Set up the Touchline devices."""
|
||||
|
||||
Touchline now uses config entries. If an entry exists in configuration.yaml,
|
||||
the import flow will attempt to import it and create a config entry.
|
||||
"""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={CONF_HOST: config[CONF_HOST]},
|
||||
)
|
||||
if (
|
||||
result.get("type") is FlowResultType.ABORT
|
||||
and result.get("reason") != "already_configured"
|
||||
):
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_import_issue_{result.get('reason')}",
|
||||
breaks_in_ha_version="2026.10.0",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=f"deprecated_yaml_import_issue_{result.get('reason')}",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Roth Touchline",
|
||||
},
|
||||
)
|
||||
return
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2026.10.0",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Roth Touchline",
|
||||
},
|
||||
)
|
||||
host = config[CONF_HOST]
|
||||
py_touchline = PyTouchline(url=host)
|
||||
number_of_devices = int(py_touchline.get_number_of_devices())
|
||||
devices = [
|
||||
Touchline(PyTouchline(id=device_id, url=host))
|
||||
for device_id in range(number_of_devices)
|
||||
]
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class Touchline(ClimateEntity):
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
"""Config flow for Roth Touchline integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pytouchline_extended import PyTouchline
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def fetch_unique_id(host: str) -> str:
|
||||
"""Fetch the unique id for the Touchline controller."""
|
||||
client = PyTouchline(url=host)
|
||||
client.get_number_of_devices()
|
||||
client.update()
|
||||
return str(client.get_controller_id())
|
||||
|
||||
|
||||
async def _async_validate_input(hass: HomeAssistant, data: dict[str, Any]) -> str:
|
||||
"""Validate the user input allows us to connect."""
|
||||
host = data[CONF_HOST]
|
||||
|
||||
try:
|
||||
return await hass.async_add_executor_job(fetch_unique_id, host)
|
||||
except (OSError, ConnectionError, TimeoutError) as err:
|
||||
_LOGGER.debug(
|
||||
"Error while connecting to Touchline controller at %s", host, exc_info=True
|
||||
)
|
||||
raise CannotConnect from err
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class TouchlineConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Roth Touchline."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
|
||||
try:
|
||||
unique_id = await _async_validate_input(self.hass, user_input)
|
||||
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if not errors:
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_HOST], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle import from YAML."""
|
||||
|
||||
# Abort if an entry with the same host already exists, to avoid duplicates
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
|
||||
# Validate the user input allows us to connect
|
||||
try:
|
||||
unique_id = await _async_validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except Exception: # noqa: BLE001
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_HOST],
|
||||
data=user_input,
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
"""Constants for the Roth Touchline integration."""
|
||||
|
||||
DOMAIN = "touchline"
|
||||
@@ -1,19 +0,0 @@
|
||||
"""Custom types for Touchline."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pytouchline_extended import PyTouchline
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
type TouchlineConfigEntry = ConfigEntry[TouchlineData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TouchlineData:
|
||||
"""Runtime data for the Touchline integration."""
|
||||
|
||||
touchline: PyTouchline
|
||||
number_of_devices: int
|
||||
@@ -2,10 +2,9 @@
|
||||
"domain": "touchline",
|
||||
"name": "Roth Touchline",
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/touchline",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pytouchline_extended"],
|
||||
"loggers": ["pytouchline"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pytouchline_extended==0.4.5"]
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Roth Touchline controller."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"description": "Home Assistant could not connect to the Roth Touchline controller while importing your YAML configuration. Remove the YAML configuration for Roth Touchline from configuration.yaml and set up the integration again from the Home Assistant UI.",
|
||||
"title": "Roth Touchline YAML configuration import failed"
|
||||
},
|
||||
"deprecated_yaml_import_issue_unknown": {
|
||||
"description": "An unknown error occurred while importing your Roth Touchline YAML configuration. Remove the YAML configuration for Roth Touchline from configuration.yaml and set up the integration again from the Home Assistant UI.",
|
||||
"title": "Roth Touchline YAML configuration import issue"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ from steamloop import (
|
||||
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import CONF_SECRET_KEY, DOMAIN, MANUFACTURER
|
||||
@@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TraneConfigEntry) -> boo
|
||||
) from err
|
||||
except AuthenticationError as err:
|
||||
await conn.disconnect()
|
||||
raise ConfigEntryError(
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_failed",
|
||||
) from err
|
||||
|
||||
@@ -121,7 +121,9 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity):
|
||||
state_wrapper: DeviceWrapper[TuyaAlarmControlPanelState],
|
||||
) -> None:
|
||||
"""Init Tuya Alarm."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._action_wrapper = action_wrapper
|
||||
self._changed_by_wrapper = changed_by_wrapper
|
||||
self._state_wrapper = state_wrapper
|
||||
|
||||
@@ -449,7 +449,9 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity):
|
||||
dpcode_wrapper: DeviceWrapper[bool],
|
||||
) -> None:
|
||||
"""Init Tuya binary sensor."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._dpcode_wrapper = dpcode_wrapper
|
||||
|
||||
@property
|
||||
|
||||
@@ -111,7 +111,9 @@ class TuyaButtonEntity(TuyaEntity, ButtonEntity):
|
||||
dpcode_wrapper: DeviceWrapper[bool],
|
||||
) -> None:
|
||||
"""Init Tuya button."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._dpcode_wrapper = dpcode_wrapper
|
||||
|
||||
async def async_press(self) -> None:
|
||||
|
||||
@@ -79,27 +79,27 @@ class TuyaClimateEntityDescription(ClimateEntityDescription):
|
||||
|
||||
CLIMATE_DESCRIPTIONS: dict[DeviceCategory, TuyaClimateEntityDescription] = {
|
||||
DeviceCategory.DBL: TuyaClimateEntityDescription(
|
||||
key="",
|
||||
key="dbl",
|
||||
switch_only_hvac_mode=HVACMode.HEAT,
|
||||
),
|
||||
DeviceCategory.KT: TuyaClimateEntityDescription(
|
||||
key="",
|
||||
key="kt",
|
||||
switch_only_hvac_mode=HVACMode.COOL,
|
||||
),
|
||||
DeviceCategory.QN: TuyaClimateEntityDescription(
|
||||
key="",
|
||||
key="qn",
|
||||
switch_only_hvac_mode=HVACMode.HEAT,
|
||||
),
|
||||
DeviceCategory.RS: TuyaClimateEntityDescription(
|
||||
key="",
|
||||
key="rs",
|
||||
switch_only_hvac_mode=HVACMode.HEAT,
|
||||
),
|
||||
DeviceCategory.WK: TuyaClimateEntityDescription(
|
||||
key="",
|
||||
key="wk",
|
||||
switch_only_hvac_mode=HVACMode.HEAT_COOL,
|
||||
),
|
||||
DeviceCategory.WKF: TuyaClimateEntityDescription(
|
||||
key="",
|
||||
key="wkf",
|
||||
switch_only_hvac_mode=HVACMode.HEAT,
|
||||
),
|
||||
}
|
||||
@@ -258,7 +258,6 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||
|
||||
entity_description: TuyaClimateEntityDescription
|
||||
_attr_name = None
|
||||
_attr_target_temperature_step = 1.0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -278,7 +277,10 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||
temperature_unit: UnitOfTemperature,
|
||||
) -> None:
|
||||
"""Determine which values to use."""
|
||||
super().__init__(device, device_manager, description)
|
||||
self._attr_target_temperature_step = 1.0
|
||||
self.entity_description = description
|
||||
|
||||
super().__init__(device, device_manager)
|
||||
self._current_humidity_wrapper = current_humidity_wrapper
|
||||
self._current_temperature = current_temperature_wrapper
|
||||
self._fan_mode_wrapper = fan_mode_wrapper
|
||||
|
||||
@@ -240,7 +240,9 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
||||
tilt_position: DeviceWrapper[int] | None,
|
||||
) -> None:
|
||||
"""Init Tuya Cover."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._attr_supported_features = CoverEntityFeature(0)
|
||||
|
||||
self._current_position = current_position or set_position
|
||||
|
||||
@@ -9,7 +9,7 @@ from tuya_sharing import CustomerDevice, Manager
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY
|
||||
|
||||
@@ -20,21 +20,13 @@ class TuyaEntity(Entity):
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: CustomerDevice,
|
||||
device_manager: Manager,
|
||||
description: EntityDescription | None = None,
|
||||
) -> None:
|
||||
def __init__(self, device: CustomerDevice, device_manager: Manager) -> None:
|
||||
"""Init TuyaHaEntity."""
|
||||
self._attr_unique_id = f"tuya.{device.id}"
|
||||
# TuyaEntity initialize mq can subscribe
|
||||
device.set_up = True
|
||||
self.device = device
|
||||
self.device_manager = device_manager
|
||||
if description:
|
||||
self._attr_unique_id = f"tuya.{device.id}{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
|
||||
@@ -161,7 +161,9 @@ class TuyaEventEntity(TuyaEntity, EventEntity):
|
||||
dpcode_wrapper: DeviceWrapper[tuple[str, dict[str, Any] | None]],
|
||||
) -> None:
|
||||
"""Init Tuya event entity."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._dpcode_wrapper = dpcode_wrapper
|
||||
self._attr_event_types = dpcode_wrapper.options
|
||||
|
||||
|
||||
@@ -137,7 +137,9 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
|
||||
target_humidity_wrapper: DeviceWrapper[int] | None = None,
|
||||
) -> None:
|
||||
"""Init Tuya (de)humidifier."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
|
||||
self._current_humidity_wrapper = current_humidity_wrapper
|
||||
self._mode_wrapper = mode_wrapper
|
||||
|
||||
@@ -529,7 +529,9 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
||||
switch_wrapper: DeviceWrapper[bool],
|
||||
) -> None:
|
||||
"""Init TuyaHaLight."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._brightness_wrapper = brightness_wrapper
|
||||
self._color_data_wrapper = color_data_wrapper
|
||||
self._color_mode_wrapper = color_mode_wrapper
|
||||
|
||||
@@ -492,7 +492,9 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity):
|
||||
dpcode_wrapper: DeviceWrapper[float],
|
||||
) -> None:
|
||||
"""Init Tuya sensor."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._dpcode_wrapper = dpcode_wrapper
|
||||
|
||||
self._attr_native_max_value = dpcode_wrapper.max_value
|
||||
|
||||
@@ -397,7 +397,9 @@ class TuyaSelectEntity(TuyaEntity, SelectEntity):
|
||||
dpcode_wrapper: DeviceWrapper[str],
|
||||
) -> None:
|
||||
"""Init Tuya sensor."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._dpcode_wrapper = dpcode_wrapper
|
||||
self._attr_options = dpcode_wrapper.options
|
||||
|
||||
|
||||
@@ -1684,7 +1684,9 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
|
||||
dpcode_wrapper: DeviceWrapper[StateType],
|
||||
) -> None:
|
||||
"""Init Tuya sensor."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._dpcode_wrapper = dpcode_wrapper
|
||||
|
||||
if description.native_unit_of_measurement is None:
|
||||
|
||||
@@ -98,7 +98,9 @@ class TuyaSirenEntity(TuyaEntity, SirenEntity):
|
||||
dpcode_wrapper: DeviceWrapper[bool],
|
||||
) -> None:
|
||||
"""Init Tuya Siren."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._dpcode_wrapper = dpcode_wrapper
|
||||
|
||||
@property
|
||||
|
||||
@@ -950,7 +950,9 @@ class TuyaSwitchEntity(TuyaEntity, SwitchEntity):
|
||||
dpcode_wrapper: DeviceWrapper[bool],
|
||||
) -> None:
|
||||
"""Init TuyaHaSwitch."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._dpcode_wrapper = dpcode_wrapper
|
||||
|
||||
@property
|
||||
|
||||
@@ -126,7 +126,9 @@ class TuyaValveEntity(TuyaEntity, ValveEntity):
|
||||
dpcode_wrapper: DeviceWrapper[bool],
|
||||
) -> None:
|
||||
"""Init TuyaValveEntity."""
|
||||
super().__init__(device, device_manager, description)
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._dpcode_wrapper = dpcode_wrapper
|
||||
|
||||
@property
|
||||
|
||||
@@ -212,6 +212,9 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]):
|
||||
async def _handle_insights_add(self, msg: WebsocketMessage) -> None:
|
||||
"""Handle access insights events (entry/exit)."""
|
||||
insights = cast(InsightsAdd, msg)
|
||||
door = insights.data.metadata.door
|
||||
if not door.id:
|
||||
return
|
||||
event_type = (
|
||||
"access_granted" if insights.data.result == "ACCESS" else "access_denied"
|
||||
)
|
||||
@@ -222,9 +225,7 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]):
|
||||
attrs["authentication"] = insights.data.metadata.authentication.display_name
|
||||
if insights.data.result:
|
||||
attrs["result"] = insights.data.result
|
||||
for door in insights.data.metadata.door:
|
||||
if door.id:
|
||||
self._dispatch_door_event(door.id, "access", event_type, attrs)
|
||||
self._dispatch_door_event(door.id, "access", event_type, attrs)
|
||||
|
||||
@callback
|
||||
def _dispatch_door_event(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["unifi_access_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["py-unifi-access==1.1.3"]
|
||||
"requirements": ["py-unifi-access==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import logging
|
||||
|
||||
import pyvera as veraApi
|
||||
from requests.exceptions import RequestException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_EXCLUDE,
|
||||
@@ -19,6 +21,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .common import (
|
||||
ControllerData,
|
||||
@@ -32,7 +35,41 @@ from .const import CONF_CONTROLLER, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
VERA_ID_LIST_SCHEMA = vol.Schema([int])
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
vol.All(
|
||||
cv.deprecated(DOMAIN),
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_CONTROLLER): cv.url,
|
||||
vol.Optional(CONF_EXCLUDE, default=[]): VERA_ID_LIST_SCHEMA,
|
||||
vol.Optional(CONF_LIGHTS, default=[]): VERA_ID_LIST_SCHEMA,
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
|
||||
"""Set up for Vera controllers."""
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
if not (config := base_config.get(DOMAIN)):
|
||||
return True
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -46,7 +46,7 @@ def set_controller_data(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, data: ControllerData
|
||||
) -> None:
|
||||
"""Set controller data in hass data."""
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = data
|
||||
hass.data[DOMAIN][config_entry.entry_id] = data
|
||||
|
||||
|
||||
class SubscriptionRegistry(pv.AbstractSubscriptionRegistry):
|
||||
|
||||
@@ -12,6 +12,7 @@ from requests.exceptions import RequestException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IMPORT,
|
||||
SOURCE_USER,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
@@ -20,6 +21,7 @@ from homeassistant.config_entries import (
|
||||
)
|
||||
from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN
|
||||
@@ -129,6 +131,31 @@ class VeraFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by import."""
|
||||
|
||||
# If there are entities with the legacy unique_id, then this imported config
|
||||
# should also use the legacy unique_id for entity creation.
|
||||
entity_registry = er.async_get(self.hass)
|
||||
use_legacy_unique_id = (
|
||||
len(
|
||||
[
|
||||
entry
|
||||
for entry in entity_registry.entities.values()
|
||||
if entry.platform == DOMAIN and entry.unique_id.isdigit()
|
||||
]
|
||||
)
|
||||
> 0
|
||||
)
|
||||
|
||||
return await self.async_step_finish(
|
||||
{
|
||||
**import_data,
|
||||
CONF_SOURCE: SOURCE_IMPORT,
|
||||
CONF_LEGACY_UNIQUE_ID: use_legacy_unique_id,
|
||||
}
|
||||
)
|
||||
|
||||
async def async_step_finish(self, config: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Validate and create config entry."""
|
||||
base_url = config[CONF_CONTROLLER] = config[CONF_CONTROLLER].rstrip("/")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user