mirror of
https://github.com/home-assistant/core.git
synced 2026-04-20 00:19:02 +02:00
Compare commits
401 Commits
synesthesi
...
2026.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f886b03e14 | ||
|
|
190ee49e3a | ||
|
|
f7c5a51f46 | ||
|
|
e4e9c22016 | ||
|
|
f2df848e3f | ||
|
|
cdce98faaf | ||
|
|
fde103cdfd | ||
|
|
fcd6c6e335 | ||
|
|
8f2cec26e3 | ||
|
|
05463cde99 | ||
|
|
a948799a6e | ||
|
|
624fab064a | ||
|
|
a331cb7199 | ||
|
|
7d6eaf40a6 | ||
|
|
1ae9e7c87d | ||
|
|
6bcfc32d48 | ||
|
|
0b5f85bdb9 | ||
|
|
d153eee822 | ||
|
|
afcc2113ce | ||
|
|
ae5bd63993 | ||
|
|
78107c478d | ||
|
|
84490ef0bb | ||
|
|
887e14638b | ||
|
|
818bde1d5e | ||
|
|
83da18b761 | ||
|
|
bd904caea1 | ||
|
|
500f030eaa | ||
|
|
ce755f5f8f | ||
|
|
fb766d164b | ||
|
|
394670e33f | ||
|
|
f79285f9ab | ||
|
|
a422611ada | ||
|
|
4c34dcd560 | ||
|
|
1aca993c12 | ||
|
|
a8cc099b66 | ||
|
|
c56d67c02f | ||
|
|
0ce98cfb34 | ||
|
|
4a13ab9aff | ||
|
|
dc65646d8b | ||
|
|
39fbdad775 | ||
|
|
b4f6a43a14 | ||
|
|
e5ff7a9944 | ||
|
|
ca9945f750 | ||
|
|
b028e2a6ae | ||
|
|
6f4aca495b | ||
|
|
a892b5364d | ||
|
|
f57e682a98 | ||
|
|
3493517b6d | ||
|
|
b5842b8484 | ||
|
|
3333b8d019 | ||
|
|
745860553c | ||
|
|
7188a09a59 | ||
|
|
96a9b89412 | ||
|
|
586d7ab526 | ||
|
|
5f2fe4ffd4 | ||
|
|
040192c103 | ||
|
|
e85430105e | ||
|
|
5c7c0a6e83 | ||
|
|
c7bd673d01 | ||
|
|
6d3a93df81 | ||
|
|
6a934b5fe3 | ||
|
|
d644348dc8 | ||
|
|
dbfde9266c | ||
|
|
ed0b68ec4a | ||
|
|
c32d523f63 | ||
|
|
98a4e27e35 | ||
|
|
fb1365e9a4 | ||
|
|
850b034a5f | ||
|
|
b880876e0e | ||
|
|
ab601e5717 | ||
|
|
7eda592c72 | ||
|
|
b981ece163 | ||
|
|
7ea931fdc8 | ||
|
|
f3038a20af | ||
|
|
de234c7190 | ||
|
|
399681984f | ||
|
|
5ca14ca7d7 | ||
|
|
ac53cfa85a | ||
|
|
02f1a9c3a9 | ||
|
|
f93fdceac9 | ||
|
|
711a89f7b8 | ||
|
|
19e58c554e | ||
|
|
feb6c2bfe6 | ||
|
|
6bb91422ff | ||
|
|
3bd699285b | ||
|
|
6d10305197 | ||
|
|
42a9c8488d | ||
|
|
c6c273559e | ||
|
|
f7394ce302 | ||
|
|
175dec6f1a | ||
|
|
d137761cb5 | ||
|
|
8055cbc58d | ||
|
|
c9dff27590 | ||
|
|
c913a858b6 | ||
|
|
4ed33a804e | ||
|
|
8bf5674826 | ||
|
|
b8a0b0083b | ||
|
|
a57c101b5e | ||
|
|
957b8c1c52 | ||
|
|
bb002d051b | ||
|
|
2b2fd4ac92 | ||
|
|
f4c270629b | ||
|
|
0e5fc44af3 | ||
|
|
803531125b | ||
|
|
c70ddd559b | ||
|
|
c06d898b00 | ||
|
|
c6233d02e8 | ||
|
|
37e69cad16 | ||
|
|
b14e729b2d | ||
|
|
87e0f2d36c | ||
|
|
ae60135a08 | ||
|
|
3ed2dccbec | ||
|
|
689ee7c1e7 | ||
|
|
12d6d7ef88 | ||
|
|
4f88c5ed29 | ||
|
|
35826dfd14 | ||
|
|
12dc33eabc | ||
|
|
9650aea6a1 | ||
|
|
aaff319e70 | ||
|
|
d9babc37f0 | ||
|
|
a616de7452 | ||
|
|
817d3e1178 | ||
|
|
e353ed1e2e | ||
|
|
96b7210bca | ||
|
|
22a6968a08 | ||
|
|
ce8519c1b1 | ||
|
|
871d9ee0b4 | ||
|
|
11d9f236b9 | ||
|
|
8be6f441dd | ||
|
|
d432092296 | ||
|
|
4d168023a2 | ||
|
|
d4d639dfa2 | ||
|
|
92375078c0 | ||
|
|
fc6efac559 | ||
|
|
a9e1bbd5ab | ||
|
|
dcf6416ae9 | ||
|
|
df6b2ba0cd | ||
|
|
19166e7938 | ||
|
|
3472a2bfbf | ||
|
|
8ac66e888e | ||
|
|
39f2e89c4b | ||
|
|
fa0ea041ad | ||
|
|
46b1981b77 | ||
|
|
29980d69b5 | ||
|
|
3a81eb9552 | ||
|
|
06e8333eab | ||
|
|
8ee0b97e5f | ||
|
|
414756edc4 | ||
|
|
1355958f53 | ||
|
|
425d380d03 | ||
|
|
ff08335890 | ||
|
|
7170e3b232 | ||
|
|
6111eaa9e9 | ||
|
|
e02a9fe61e | ||
|
|
cba9bf5dc4 | ||
|
|
72a661f1fa | ||
|
|
4168000155 | ||
|
|
9d230b4f7c | ||
|
|
745f32faa3 | ||
|
|
112ad886c6 | ||
|
|
8b0ec21a15 | ||
|
|
afce52a0f4 | ||
|
|
7e4757c213 | ||
|
|
d6dbcc8d82 | ||
|
|
fca87a2b8a | ||
|
|
87e648b8b8 | ||
|
|
ada549489c | ||
|
|
15e13de2a6 | ||
|
|
dd74665622 | ||
|
|
ff8fc56696 | ||
|
|
2d8c903533 | ||
|
|
c1606f515b | ||
|
|
fac2702063 | ||
|
|
76ae6958ed | ||
|
|
1876ed7d16 | ||
|
|
08ef4e0de0 | ||
|
|
a48db9d817 | ||
|
|
1334531740 | ||
|
|
d769b16ada | ||
|
|
c830320730 | ||
|
|
336aa0f5df | ||
|
|
754291b34f | ||
|
|
bbae0862b0 | ||
|
|
6b7693b2fd | ||
|
|
954926a05c | ||
|
|
71981f66ec | ||
|
|
7f94f95ac9 | ||
|
|
4ee3177c5d | ||
|
|
9c1f9ca5c6 | ||
|
|
cff4cf4d2c | ||
|
|
ee9d9781ee | ||
|
|
1b972d4adc | ||
|
|
72598479d5 | ||
|
|
02599a4a6e | ||
|
|
af9f351fce | ||
|
|
ff79943776 | ||
|
|
e60048ef30 | ||
|
|
24c0b22038 | ||
|
|
6f32a53742 | ||
|
|
da9d1080d9 | ||
|
|
2ea4d7913e | ||
|
|
16999e3707 | ||
|
|
5c53b847dc | ||
|
|
3afd763d16 | ||
|
|
75a15ed24e | ||
|
|
6d56597a2a | ||
|
|
5872222213 | ||
|
|
bd5c73fd7b | ||
|
|
d8a32dcf69 | ||
|
|
87cd90ab5d | ||
|
|
cb5b0c5b5e | ||
|
|
2fa16101f4 | ||
|
|
6dd5c30b49 | ||
|
|
72f5a572eb | ||
|
|
d501d8cb28 | ||
|
|
35c4b4ff5b | ||
|
|
f3e8ac5b8e | ||
|
|
ab2bcd84c6 | ||
|
|
cdf7b013a9 | ||
|
|
eeba0467a1 | ||
|
|
43ca72bf7e | ||
|
|
aa9e279026 | ||
|
|
9f3917830d | ||
|
|
c458bc2ee3 | ||
|
|
e0455629d7 | ||
|
|
b802dcba8d | ||
|
|
7ff868e94c | ||
|
|
44bd3e3d74 | ||
|
|
9d793ce1df | ||
|
|
d8dee8fc91 | ||
|
|
3c52acb825 | ||
|
|
cb195be6ad | ||
|
|
08f7bed679 | ||
|
|
744563c7a7 | ||
|
|
5d48801645 | ||
|
|
4211686c07 | ||
|
|
98379c9642 | ||
|
|
a3c9d35a13 | ||
|
|
5a7abc0a92 | ||
|
|
ade73ec159 | ||
|
|
6f7a5d9320 | ||
|
|
f30217aa41 | ||
|
|
4d565e6089 | ||
|
|
faaa87e36f | ||
|
|
cd142833e7 | ||
|
|
434e1e5a69 | ||
|
|
a0ef23097f | ||
|
|
4d7bd49d2c | ||
|
|
a73157e739 | ||
|
|
6260bd9abc | ||
|
|
ec7aaeb8e2 | ||
|
|
81e92e2567 | ||
|
|
92fed08095 | ||
|
|
6c1ad5aba4 | ||
|
|
6b1a5219a3 | ||
|
|
b3efa472b5 | ||
|
|
2cc8934bbd | ||
|
|
a22083de10 | ||
|
|
2c8b8007c1 | ||
|
|
c815090ece | ||
|
|
94acb8102f | ||
|
|
8c73dcad91 | ||
|
|
c8f7d9dd42 | ||
|
|
b522db1daf | ||
|
|
338836cba2 | ||
|
|
f5e7605502 | ||
|
|
22ddb18ce2 | ||
|
|
b541dc0a97 | ||
|
|
15d0a01833 | ||
|
|
71be2073eb | ||
|
|
e6886fc562 | ||
|
|
7f0f038bcd | ||
|
|
686ab66a52 | ||
|
|
7a4f953fa6 | ||
|
|
cd0834bfbe | ||
|
|
c598aa6964 | ||
|
|
5ef28932e5 | ||
|
|
f2eac87673 | ||
|
|
aeb920e8ef | ||
|
|
8540a27f0d | ||
|
|
fe2d8a31b8 | ||
|
|
f4efc929d6 | ||
|
|
15d7febffd | ||
|
|
0a8f5449f2 | ||
|
|
d2179d9243 | ||
|
|
bf1327e355 | ||
|
|
9afa827eab | ||
|
|
3ae6f8e7a0 | ||
|
|
56962ff907 | ||
|
|
719b9bdc3c | ||
|
|
bb1dc51a6b | ||
|
|
abbbb7df13 | ||
|
|
5a308d11e4 | ||
|
|
6bf487c3f3 | ||
|
|
3162b637ea | ||
|
|
8cc1dd8091 | ||
|
|
83ff038188 | ||
|
|
13a8d7f7a8 | ||
|
|
a721d32889 | ||
|
|
bce65d4f35 | ||
|
|
daa0ddffb9 | ||
|
|
ee7dd329f0 | ||
|
|
00cd07736e | ||
|
|
78871e1766 | ||
|
|
bb6f739861 | ||
|
|
9948431012 | ||
|
|
4f9241be79 | ||
|
|
5215e674b1 | ||
|
|
31b12701dc | ||
|
|
d5ff890a18 | ||
|
|
32221a1ec4 | ||
|
|
a6dd56eed0 | ||
|
|
682eba9773 | ||
|
|
c055972887 | ||
|
|
78e2514b46 | ||
|
|
0af6a86507 | ||
|
|
2367d7c168 | ||
|
|
8d91fd0655 | ||
|
|
171b8dfa89 | ||
|
|
f299b009fa | ||
|
|
91e9eb0ab3 | ||
|
|
a2b91a9ac0 | ||
|
|
a3add179a0 | ||
|
|
6075becbab | ||
|
|
193f519366 | ||
|
|
b6508c2ca4 | ||
|
|
3dc478a357 | ||
|
|
bd407872b0 | ||
|
|
8b696044c3 | ||
|
|
1a772b6df2 | ||
|
|
a880ad2904 | ||
|
|
ea73f2d0f1 | ||
|
|
11351500ea | ||
|
|
86901bfd80 | ||
|
|
d2ef60125f | ||
|
|
471b49f12b | ||
|
|
33e9e663da | ||
|
|
31ff44f1a6 | ||
|
|
9274bd7867 | ||
|
|
e36f9eb639 | ||
|
|
5149932ec8 | ||
|
|
bdd3fc7059 | ||
|
|
c795cbc5a3 | ||
|
|
20dd604292 | ||
|
|
c35a6dc044 | ||
|
|
cbe767c9c5 | ||
|
|
eea3b78665 | ||
|
|
a78a553bab | ||
|
|
7c7af7f0df | ||
|
|
d52ad38dca | ||
|
|
477384ce9b | ||
|
|
d1be6e1c68 | ||
|
|
151eae4d5a | ||
|
|
035e0042fa | ||
|
|
2568db5fdf | ||
|
|
28b1ded702 | ||
|
|
236cd795b9 | ||
|
|
65e90b9b9f | ||
|
|
96c3f3f054 | ||
|
|
bd8e90bb00 | ||
|
|
d488bdad8a | ||
|
|
dec6f955f3 | ||
|
|
bdb74ca37a | ||
|
|
14c0a82284 | ||
|
|
b42bd4909b | ||
|
|
001a1aada6 | ||
|
|
cd28c924ac | ||
|
|
a19c1a7ba1 | ||
|
|
e0d3298e77 | ||
|
|
2296c92a3e | ||
|
|
66311508ad | ||
|
|
d628463471 | ||
|
|
a5f9c400cc | ||
|
|
36051d015a | ||
|
|
65ae221ba7 | ||
|
|
0fd9360249 | ||
|
|
55f56c6632 | ||
|
|
0336ffca77 | ||
|
|
f33bd2de22 | ||
|
|
0599550e04 | ||
|
|
c384d41625 | ||
|
|
57b0456760 | ||
|
|
85c9b00035 | ||
|
|
d9df5f1fab | ||
|
|
f3cea5160b | ||
|
|
ac7b5a2957 | ||
|
|
031830f004 | ||
|
|
39a655e100 | ||
|
|
714411c072 | ||
|
|
94eb1031cc | ||
|
|
fa98eb52ad | ||
|
|
7b1fbbd278 | ||
|
|
b518729367 | ||
|
|
d04c5ccc44 | ||
|
|
d8ba32bc8e | ||
|
|
7ae3c2012d | ||
|
|
05b78a22cf | ||
|
|
0a5589c800 | ||
|
|
9fb5bceeef | ||
|
|
f4cce71d1f | ||
|
|
2209c9e0f7 |
7
.github/workflows/builder.yml
vendored
7
.github/workflows/builder.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Get information
|
||||
id: info
|
||||
uses: home-assistant/actions/helpers/info@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/info@5f5b077d63a1e4c53019231409a0c4d791fb74e5 # zizmor: ignore[unpinned-uses]
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
@@ -224,7 +224,6 @@ jobs:
|
||||
matrix:
|
||||
machine:
|
||||
- generic-x86-64
|
||||
- intel-nuc
|
||||
- khadas-vim3
|
||||
- odroid-c2
|
||||
- odroid-c4
|
||||
@@ -248,10 +247,6 @@ jobs:
|
||||
- machine: qemux86-64
|
||||
arch: amd64
|
||||
runs-on: ubuntu-24.04
|
||||
# TODO: remove, intel-nuc is a legacy name for x86-64, renamed in 2021
|
||||
- machine: intel-nuc
|
||||
arch: amd64
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
@@ -174,7 +174,6 @@ homeassistant.components.dnsip.*
|
||||
homeassistant.components.doorbird.*
|
||||
homeassistant.components.dormakaba_dkey.*
|
||||
homeassistant.components.downloader.*
|
||||
homeassistant.components.dropbox.*
|
||||
homeassistant.components.droplet.*
|
||||
homeassistant.components.dsmr.*
|
||||
homeassistant.components.duckdns.*
|
||||
@@ -274,10 +273,12 @@ homeassistant.components.homekit_controller.storage
|
||||
homeassistant.components.homekit_controller.utils
|
||||
homeassistant.components.homewizard.*
|
||||
homeassistant.components.homeworks.*
|
||||
homeassistant.components.hr_energy_qube.*
|
||||
homeassistant.components.http.*
|
||||
homeassistant.components.huawei_lte.*
|
||||
homeassistant.components.humidifier.*
|
||||
homeassistant.components.husqvarna_automower.*
|
||||
homeassistant.components.huum.*
|
||||
homeassistant.components.hydrawise.*
|
||||
homeassistant.components.hyperion.*
|
||||
homeassistant.components.hypontech.*
|
||||
@@ -327,6 +328,7 @@ homeassistant.components.ld2410_ble.*
|
||||
homeassistant.components.led_ble.*
|
||||
homeassistant.components.lektrico.*
|
||||
homeassistant.components.letpot.*
|
||||
homeassistant.components.lg_infrared.*
|
||||
homeassistant.components.libre_hardware_monitor.*
|
||||
homeassistant.components.lidarr.*
|
||||
homeassistant.components.lifx.*
|
||||
|
||||
12
CODEOWNERS
generated
12
CODEOWNERS
generated
@@ -401,8 +401,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/dremel_3d_printer/ @tkdrob
|
||||
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/homeassistant/components/dropbox/ @bdr99
|
||||
/tests/components/dropbox/ @bdr99
|
||||
/homeassistant/components/droplet/ @sarahseidman
|
||||
/tests/components/droplet/ @sarahseidman
|
||||
/homeassistant/components/dsmr/ @Robbie1221
|
||||
@@ -739,6 +737,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/homewizard/ @DCSBL
|
||||
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
||||
/tests/components/honeywell/ @rdfurman @mkmer
|
||||
/homeassistant/components/hr_energy_qube/ @MattieGit
|
||||
/tests/components/hr_energy_qube/ @MattieGit
|
||||
/homeassistant/components/html5/ @alexyao2015
|
||||
/tests/components/html5/ @alexyao2015
|
||||
/homeassistant/components/http/ @home-assistant/core
|
||||
@@ -947,6 +947,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/lektrico/ @lektrico
|
||||
/homeassistant/components/letpot/ @jpelgrom
|
||||
/tests/components/letpot/ @jpelgrom
|
||||
/homeassistant/components/lg_infrared/ @home-assistant/core
|
||||
/tests/components/lg_infrared/ @home-assistant/core
|
||||
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||
@@ -1075,6 +1077,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/modern_forms/ @wonderslug
|
||||
/homeassistant/components/moehlenhoff_alpha2/ @j-a-n
|
||||
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
||||
/homeassistant/components/moisture/ @home-assistant/core
|
||||
/tests/components/moisture/ @home-assistant/core
|
||||
/homeassistant/components/monarch_money/ @jeeftor
|
||||
/tests/components/monarch_money/ @jeeftor
|
||||
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
||||
@@ -1600,6 +1604,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
||||
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
|
||||
/tests/components/solarlog/ @Ernst79 @dontinelli
|
||||
/homeassistant/components/solarman/ @solarmanpv
|
||||
/tests/components/solarman/ @solarmanpv
|
||||
/homeassistant/components/solax/ @squishykid @Darsstar
|
||||
/tests/components/solax/ @squishykid @Darsstar
|
||||
/homeassistant/components/soma/ @ratsept
|
||||
@@ -1756,6 +1762,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tomorrowio/ @raman325 @lymanepp
|
||||
/homeassistant/components/totalconnect/ @austinmroczek
|
||||
/tests/components/totalconnect/ @austinmroczek
|
||||
/homeassistant/components/touchline/ @mnordseth
|
||||
/tests/components/touchline/ @mnordseth
|
||||
/homeassistant/components/touchline_sl/ @jnsgruk
|
||||
/tests/components/touchline_sl/ @jnsgruk
|
||||
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
||||
|
||||
2
Dockerfile
generated
2
Dockerfile
generated
@@ -29,7 +29,7 @@ RUN \
|
||||
# Verify go2rtc can be executed
|
||||
go2rtc --version \
|
||||
# Install uv
|
||||
&& pip3 install uv==0.10.6
|
||||
&& pip3 install uv==0.11.1
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
||||
@@ -238,7 +238,9 @@ DEFAULT_INTEGRATIONS = {
|
||||
"timer",
|
||||
#
|
||||
# Base platforms:
|
||||
*BASE_PLATFORMS,
|
||||
# Note: Calendar and todo are not included to prevent them from registering
|
||||
# their frontend panels when there are no calendar or todo integrations.
|
||||
*(BASE_PLATFORMS - {"calendar", "todo"}),
|
||||
#
|
||||
# Integrations providing triggers and conditions for base platforms:
|
||||
"air_quality",
|
||||
@@ -248,6 +250,7 @@ DEFAULT_INTEGRATIONS = {
|
||||
"gate",
|
||||
"humidity",
|
||||
"illuminance",
|
||||
"moisture",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"power",
|
||||
@@ -467,6 +470,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> bool:
|
||||
translation.async_setup(hass)
|
||||
|
||||
recovery = hass.config.recovery_mode
|
||||
device_registry.async_setup(hass)
|
||||
try:
|
||||
await asyncio.gather(
|
||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||
|
||||
5
homeassistant/brands/bega.json
Normal file
5
homeassistant/brands/bega.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "bega",
|
||||
"name": "BEGA",
|
||||
"iot_standards": ["zigbee"]
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"domain": "lg",
|
||||
"name": "LG",
|
||||
"integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
|
||||
"integrations": [
|
||||
"lg_infrared",
|
||||
"lg_netcast",
|
||||
"lg_soundbar",
|
||||
"lg_thinq",
|
||||
"webostv"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
},
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["jaraco.abode", "lomond"],
|
||||
"requirements": ["jaraco.abode==6.2.1"],
|
||||
"requirements": ["jaraco.abode==6.4.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
||||
@@ -40,6 +41,7 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
||||
AbodeSensorDescription(
|
||||
key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
|
||||
device.temp_unit
|
||||
],
|
||||
@@ -48,12 +50,14 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
||||
AbodeSensorDescription(
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
|
||||
value_fn=lambda device: cast(float, device.humidity),
|
||||
),
|
||||
AbodeSensorDescription(
|
||||
key="lux",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
|
||||
value_fn=lambda device: cast(float, device.lux),
|
||||
),
|
||||
|
||||
134
homeassistant/components/air_quality/condition.py
Normal file
134
homeassistant/components/air_quality/condition.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Provides conditions for air quality."""
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
make_entity_numerical_condition,
|
||||
make_entity_numerical_condition_with_unit,
|
||||
make_entity_state_condition,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import (
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
MassVolumeConcentrationConverter,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
OzoneConcentrationConverter,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
UnitlessRatioConverter,
|
||||
)
|
||||
|
||||
|
||||
def _make_detected_condition(
|
||||
device_class: BinarySensorDeviceClass,
|
||||
) -> type[Condition]:
|
||||
"""Create a detected condition for a binary sensor device class."""
|
||||
return make_entity_state_condition(
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
|
||||
)
|
||||
|
||||
|
||||
def _make_cleared_condition(
|
||||
device_class: BinarySensorDeviceClass,
|
||||
) -> type[Condition]:
|
||||
"""Create a cleared condition for a binary sensor device class."""
|
||||
return make_entity_state_condition(
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
|
||||
)
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
# Binary sensor conditions (detected/cleared)
|
||||
"is_gas_detected": _make_detected_condition(BinarySensorDeviceClass.GAS),
|
||||
"is_gas_cleared": _make_cleared_condition(BinarySensorDeviceClass.GAS),
|
||||
"is_co_detected": _make_detected_condition(BinarySensorDeviceClass.CO),
|
||||
"is_co_cleared": _make_cleared_condition(BinarySensorDeviceClass.CO),
|
||||
"is_smoke_detected": _make_detected_condition(BinarySensorDeviceClass.SMOKE),
|
||||
"is_smoke_cleared": _make_cleared_condition(BinarySensorDeviceClass.SMOKE),
|
||||
# Numerical sensor conditions with unit conversion
|
||||
"is_co_value": make_entity_numerical_condition_with_unit(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"is_ozone_value": make_entity_numerical_condition_with_unit(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
OzoneConcentrationConverter,
|
||||
),
|
||||
"is_voc_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
MassVolumeConcentrationConverter,
|
||||
),
|
||||
"is_voc_ratio_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
},
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
UnitlessRatioConverter,
|
||||
),
|
||||
"is_no_value": make_entity_numerical_condition_with_unit(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
),
|
||||
"is_no2_value": make_entity_numerical_condition_with_unit(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
),
|
||||
"is_so2_value": make_entity_numerical_condition_with_unit(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
),
|
||||
# Numerical sensor conditions without unit conversion (single-unit device classes)
|
||||
"is_co2_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"is_pm1_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm25_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm4_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm10_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_n2o_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the air quality conditions."""
|
||||
return CONDITIONS
|
||||
449
homeassistant/components/air_quality/conditions.yaml
Normal file
449
homeassistant/components/air_quality/conditions.yaml
Normal file
@@ -0,0 +1,449 @@
|
||||
# --- Common condition fields ---
|
||||
|
||||
.condition_behavior: &condition_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
# --- Unit lists for multi-unit pollutants ---
|
||||
|
||||
.co_units: &co_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "mg/m³"
|
||||
- "μg/m³"
|
||||
|
||||
.ozone_units: &ozone_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.voc_units: &voc_units
|
||||
- "μg/m³"
|
||||
- "mg/m³"
|
||||
|
||||
.voc_ratio_units: &voc_ratio_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
|
||||
.no_units: &no_units
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
.no2_units: &no2_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.so2_units: &so2_units
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
# --- Entity filter anchors ---
|
||||
|
||||
.co_threshold_entity: &co_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *co_units
|
||||
- domain: sensor
|
||||
device_class: carbon_monoxide
|
||||
- domain: number
|
||||
device_class: carbon_monoxide
|
||||
|
||||
.co2_threshold_entity: &co2_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "ppm"
|
||||
- domain: sensor
|
||||
device_class: carbon_dioxide
|
||||
- domain: number
|
||||
device_class: carbon_dioxide
|
||||
|
||||
.pm1_threshold_entity: &pm1_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm1
|
||||
- domain: number
|
||||
device_class: pm1
|
||||
|
||||
.pm25_threshold_entity: &pm25_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm25
|
||||
- domain: number
|
||||
device_class: pm25
|
||||
|
||||
.pm4_threshold_entity: &pm4_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm4
|
||||
- domain: number
|
||||
device_class: pm4
|
||||
|
||||
.pm10_threshold_entity: &pm10_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm10
|
||||
- domain: number
|
||||
device_class: pm10
|
||||
|
||||
.ozone_threshold_entity: &ozone_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *ozone_units
|
||||
- domain: sensor
|
||||
device_class: ozone
|
||||
- domain: number
|
||||
device_class: ozone
|
||||
|
||||
.voc_threshold_entity: &voc_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *voc_units
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds
|
||||
|
||||
.voc_ratio_threshold_entity: &voc_ratio_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *voc_ratio_units
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds_parts
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds_parts
|
||||
|
||||
.no_threshold_entity: &no_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *no_units
|
||||
- domain: sensor
|
||||
device_class: nitrogen_monoxide
|
||||
- domain: number
|
||||
device_class: nitrogen_monoxide
|
||||
|
||||
.no2_threshold_entity: &no2_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *no2_units
|
||||
- domain: sensor
|
||||
device_class: nitrogen_dioxide
|
||||
- domain: number
|
||||
device_class: nitrogen_dioxide
|
||||
|
||||
.n2o_threshold_entity: &n2o_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: nitrous_oxide
|
||||
- domain: number
|
||||
device_class: nitrous_oxide
|
||||
|
||||
.so2_threshold_entity: &so2_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *so2_units
|
||||
- domain: sensor
|
||||
device_class: sulphur_dioxide
|
||||
- domain: number
|
||||
device_class: sulphur_dioxide
|
||||
|
||||
# --- Number anchors for single-unit pollutants ---
|
||||
|
||||
.co2_threshold_number: &co2_threshold_number
|
||||
mode: box
|
||||
unit_of_measurement: "ppm"
|
||||
|
||||
.ugm3_threshold_number: &ugm3_threshold_number
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
|
||||
# --- Binary sensor targets ---
|
||||
|
||||
.target_gas: &target_gas
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: gas
|
||||
|
||||
.target_co_binary: &target_co_binary
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: carbon_monoxide
|
||||
|
||||
.target_smoke: &target_smoke
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: smoke
|
||||
|
||||
# --- Sensor targets ---
|
||||
|
||||
.target_co_sensor: &target_co_sensor
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: carbon_monoxide
|
||||
|
||||
.target_co2: &target_co2
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: carbon_dioxide
|
||||
|
||||
.target_pm1: &target_pm1
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: pm1
|
||||
|
||||
.target_pm25: &target_pm25
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: pm25
|
||||
|
||||
.target_pm4: &target_pm4
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: pm4
|
||||
|
||||
.target_pm10: &target_pm10
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: pm10
|
||||
|
||||
.target_ozone: &target_ozone
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: ozone
|
||||
|
||||
.target_voc: &target_voc
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds
|
||||
|
||||
.target_voc_ratio: &target_voc_ratio
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds_parts
|
||||
|
||||
.target_no: &target_no
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: nitrogen_monoxide
|
||||
|
||||
.target_no2: &target_no2
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: nitrogen_dioxide
|
||||
|
||||
.target_n2o: &target_n2o
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: nitrous_oxide
|
||||
|
||||
.target_so2: &target_so2
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: sulphur_dioxide
|
||||
|
||||
# --- Binary sensor conditions ---
|
||||
|
||||
.condition_binary_common: &condition_binary_common
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
|
||||
is_gas_detected:
|
||||
<<: *condition_binary_common
|
||||
target: *target_gas
|
||||
|
||||
is_gas_cleared:
|
||||
<<: *condition_binary_common
|
||||
target: *target_gas
|
||||
|
||||
is_co_detected:
|
||||
<<: *condition_binary_common
|
||||
target: *target_co_binary
|
||||
|
||||
is_co_cleared:
|
||||
<<: *condition_binary_common
|
||||
target: *target_co_binary
|
||||
|
||||
is_smoke_detected:
|
||||
<<: *condition_binary_common
|
||||
target: *target_smoke
|
||||
|
||||
is_smoke_cleared:
|
||||
<<: *condition_binary_common
|
||||
target: *target_smoke
|
||||
|
||||
# --- Numerical sensor conditions with unit conversion ---
|
||||
|
||||
is_co_value:
|
||||
target: *target_co_sensor
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *co_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *co_units
|
||||
|
||||
is_ozone_value:
|
||||
target: *target_ozone
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *ozone_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *ozone_units
|
||||
|
||||
is_voc_value:
|
||||
target: *target_voc
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *voc_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *voc_units
|
||||
|
||||
is_voc_ratio_value:
|
||||
target: *target_voc_ratio
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *voc_ratio_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *voc_ratio_units
|
||||
|
||||
is_no_value:
|
||||
target: *target_no
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *no_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *no_units
|
||||
|
||||
is_no2_value:
|
||||
target: *target_no2
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *no2_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *no2_units
|
||||
|
||||
is_so2_value:
|
||||
target: *target_so2
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *so2_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *so2_units
|
||||
|
||||
# --- Numerical sensor conditions without unit conversion ---
|
||||
|
||||
is_co2_value:
|
||||
target: *target_co2
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *co2_threshold_entity
|
||||
mode: is
|
||||
number: *co2_threshold_number
|
||||
|
||||
is_pm1_value:
|
||||
target: *target_pm1
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm1_threshold_entity
|
||||
mode: is
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
is_pm25_value:
|
||||
target: *target_pm25
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm25_threshold_entity
|
||||
mode: is
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
is_pm4_value:
|
||||
target: *target_pm4
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm4_threshold_entity
|
||||
mode: is
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
is_pm10_value:
|
||||
target: *target_pm10
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm10_threshold_entity
|
||||
mode: is
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
is_n2o_value:
|
||||
target: *target_n2o
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *n2o_threshold_entity
|
||||
mode: is
|
||||
number: *ugm3_threshold_number
|
||||
@@ -1,4 +1,63 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_co2_value": {
|
||||
"condition": "mdi:molecule-co2"
|
||||
},
|
||||
"is_co_cleared": {
|
||||
"condition": "mdi:check-circle"
|
||||
},
|
||||
"is_co_detected": {
|
||||
"condition": "mdi:molecule-co"
|
||||
},
|
||||
"is_co_value": {
|
||||
"condition": "mdi:molecule-co"
|
||||
},
|
||||
"is_gas_cleared": {
|
||||
"condition": "mdi:check-circle"
|
||||
},
|
||||
"is_gas_detected": {
|
||||
"condition": "mdi:gas-cylinder"
|
||||
},
|
||||
"is_n2o_value": {
|
||||
"condition": "mdi:factory"
|
||||
},
|
||||
"is_no2_value": {
|
||||
"condition": "mdi:factory"
|
||||
},
|
||||
"is_no_value": {
|
||||
"condition": "mdi:factory"
|
||||
},
|
||||
"is_ozone_value": {
|
||||
"condition": "mdi:weather-sunny-alert"
|
||||
},
|
||||
"is_pm10_value": {
|
||||
"condition": "mdi:blur"
|
||||
},
|
||||
"is_pm1_value": {
|
||||
"condition": "mdi:blur"
|
||||
},
|
||||
"is_pm25_value": {
|
||||
"condition": "mdi:blur"
|
||||
},
|
||||
"is_pm4_value": {
|
||||
"condition": "mdi:blur"
|
||||
},
|
||||
"is_smoke_cleared": {
|
||||
"condition": "mdi:check-circle"
|
||||
},
|
||||
"is_smoke_detected": {
|
||||
"condition": "mdi:smoke-detector-variant"
|
||||
},
|
||||
"is_so2_value": {
|
||||
"condition": "mdi:factory"
|
||||
},
|
||||
"is_voc_ratio_value": {
|
||||
"condition": "mdi:air-filter"
|
||||
},
|
||||
"is_voc_value": {
|
||||
"condition": "mdi:air-filter"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:air-filter"
|
||||
|
||||
@@ -1,23 +1,227 @@
|
||||
{
|
||||
"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"
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_threshold_name": "Threshold type"
|
||||
},
|
||||
"conditions": {
|
||||
"is_co2_value": {
|
||||
"description": "Tests the carbon dioxide level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon dioxide value"
|
||||
},
|
||||
"is_co_cleared": {
|
||||
"description": "Tests if one or more carbon monoxide sensors are cleared.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide cleared"
|
||||
},
|
||||
"is_co_detected": {
|
||||
"description": "Tests if one or more carbon monoxide sensors are detecting carbon monoxide.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide detected"
|
||||
},
|
||||
"is_co_value": {
|
||||
"description": "Tests the carbon monoxide level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide value"
|
||||
},
|
||||
"is_gas_cleared": {
|
||||
"description": "Tests if one or more gas sensors are cleared.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas cleared"
|
||||
},
|
||||
"is_gas_detected": {
|
||||
"description": "Tests if one or more gas sensors are detecting gas.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas detected"
|
||||
},
|
||||
"is_n2o_value": {
|
||||
"description": "Tests the nitrous oxide level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrous oxide value"
|
||||
},
|
||||
"is_no2_value": {
|
||||
"description": "Tests the nitrogen dioxide level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen dioxide value"
|
||||
},
|
||||
"is_no_value": {
|
||||
"description": "Tests the nitrogen monoxide level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen monoxide value"
|
||||
},
|
||||
"is_ozone_value": {
|
||||
"description": "Tests the ozone level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Ozone value"
|
||||
},
|
||||
"is_pm10_value": {
|
||||
"description": "Tests the PM10 level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM10 value"
|
||||
},
|
||||
"is_pm1_value": {
|
||||
"description": "Tests the PM1 level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM1 value"
|
||||
},
|
||||
"is_pm25_value": {
|
||||
"description": "Tests the PM2.5 level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM2.5 value"
|
||||
},
|
||||
"is_pm4_value": {
|
||||
"description": "Tests the PM4 level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM4 value"
|
||||
},
|
||||
"is_smoke_cleared": {
|
||||
"description": "Tests if one or more smoke sensors are cleared.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke cleared"
|
||||
},
|
||||
"is_smoke_detected": {
|
||||
"description": "Tests if one or more smoke sensors are detecting smoke.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke detected"
|
||||
},
|
||||
"is_so2_value": {
|
||||
"description": "Tests the sulphur dioxide level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Sulphur dioxide value"
|
||||
},
|
||||
"is_voc_ratio_value": {
|
||||
"description": "Tests the volatile organic compounds ratio of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds ratio value"
|
||||
},
|
||||
"is_voc_value": {
|
||||
"description": "Tests the volatile organic compounds level of one or more entities.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds value"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
"number": "Number"
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
@@ -26,14 +230,6 @@
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
},
|
||||
"trigger_threshold_type": {
|
||||
"options": {
|
||||
"above": "Above",
|
||||
"below": "Below",
|
||||
"between": "Between",
|
||||
"outside": "Outside"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Air Quality",
|
||||
@@ -41,13 +237,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon dioxide level changed"
|
||||
@@ -56,20 +247,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon dioxide level crossed threshold"
|
||||
@@ -77,17 +258,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide level changed"
|
||||
@@ -96,7 +268,6 @@
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
@@ -106,24 +277,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide level crossed threshold"
|
||||
@@ -132,7 +289,6 @@
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
@@ -142,7 +298,6 @@
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
@@ -152,7 +307,6 @@
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
@@ -161,13 +315,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrous oxide level changed"
|
||||
@@ -176,20 +325,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrous oxide level crossed threshold"
|
||||
@@ -197,17 +336,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen dioxide level changed"
|
||||
@@ -216,24 +346,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen dioxide level crossed threshold"
|
||||
@@ -241,17 +357,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen monoxide level changed"
|
||||
@@ -260,24 +367,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen monoxide level crossed threshold"
|
||||
@@ -285,17 +378,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Ozone level changed"
|
||||
@@ -304,24 +388,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Ozone level crossed threshold"
|
||||
@@ -329,13 +399,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM10 level changed"
|
||||
@@ -344,20 +409,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM10 level crossed threshold"
|
||||
@@ -365,13 +420,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM1 level changed"
|
||||
@@ -380,20 +430,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM1 level crossed threshold"
|
||||
@@ -401,13 +441,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM2.5 level changed"
|
||||
@@ -416,20 +451,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM2.5 level crossed threshold"
|
||||
@@ -437,13 +462,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM4 level changed"
|
||||
@@ -452,20 +472,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM4 level crossed threshold"
|
||||
@@ -474,7 +484,6 @@
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
@@ -484,7 +493,6 @@
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
@@ -493,17 +501,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Sulphur dioxide level changed"
|
||||
@@ -512,24 +511,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Sulphur dioxide level crossed threshold"
|
||||
@@ -537,17 +522,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds level changed"
|
||||
@@ -556,24 +532,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds level crossed threshold"
|
||||
@@ -581,17 +543,8 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds ratio changed"
|
||||
@@ -600,24 +553,10 @@
|
||||
"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%]"
|
||||
"threshold": {
|
||||
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds ratio crossed threshold"
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
EntityTargetStateTriggerBase,
|
||||
Trigger,
|
||||
@@ -64,28 +64,28 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"smoke_cleared": _make_cleared_trigger(BinarySensorDeviceClass.SMOKE),
|
||||
# Numerical sensor triggers with unit conversion
|
||||
"co_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"co_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"ozone_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
OzoneConcentrationConverter,
|
||||
),
|
||||
"ozone_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
OzoneConcentrationConverter,
|
||||
),
|
||||
"voc_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||
)
|
||||
},
|
||||
@@ -94,7 +94,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
),
|
||||
"voc_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||
)
|
||||
},
|
||||
@@ -103,7 +103,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
),
|
||||
"voc_ratio_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
},
|
||||
@@ -112,7 +112,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
),
|
||||
"voc_ratio_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
},
|
||||
@@ -120,114 +120,82 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
UnitlessRatioConverter,
|
||||
),
|
||||
"no_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
),
|
||||
"no_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
),
|
||||
"no2_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
),
|
||||
"no2_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
),
|
||||
"so2_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
),
|
||||
"so2_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
),
|
||||
# Numerical sensor triggers without unit conversion (single-unit device classes)
|
||||
"co2_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"co2_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"pm1_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm1_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm25_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm25_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm4_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm4_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm10_changed": make_entity_numerical_state_changed_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"pm10_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"n2o_changed": make_entity_numerical_state_changed_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROUS_OXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"n2o_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROUS_OXIDE
|
||||
)
|
||||
},
|
||||
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -10,374 +10,155 @@
|
||||
- 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
|
||||
# --- Unit lists for multi-unit pollutants ---
|
||||
|
||||
.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
|
||||
.co_units: &co_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "mg/m³"
|
||||
- "μg/m³"
|
||||
|
||||
.number_or_entity_pm1: &number_or_entity_pm1
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm1
|
||||
- domain: number
|
||||
device_class: pm1
|
||||
translation_key: number_or_entity
|
||||
.ozone_units: &ozone_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.number_or_entity_pm25: &number_or_entity_pm25
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm25
|
||||
- domain: number
|
||||
device_class: pm25
|
||||
translation_key: number_or_entity
|
||||
.voc_units: &voc_units
|
||||
- "μg/m³"
|
||||
- "mg/m³"
|
||||
|
||||
.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
|
||||
.voc_ratio_units: &voc_ratio_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
|
||||
.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
|
||||
.no_units: &no_units
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
.number_or_entity_ozone: &number_or_entity_ozone
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: ozone
|
||||
- domain: number
|
||||
device_class: ozone
|
||||
translation_key: number_or_entity
|
||||
.no2_units: &no2_units
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.number_or_entity_voc: &number_or_entity_voc
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "μg/m³"
|
||||
- "mg/m³"
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds
|
||||
translation_key: number_or_entity
|
||||
.so2_units: &so2_units
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
.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
|
||||
# --- Entity filter anchors ---
|
||||
|
||||
.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
|
||||
.co_threshold_entity: &co_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *co_units
|
||||
- domain: sensor
|
||||
device_class: carbon_monoxide
|
||||
- domain: number
|
||||
device_class: carbon_monoxide
|
||||
|
||||
.number_or_entity_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
|
||||
.co2_threshold_entity: &co2_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "ppm"
|
||||
- domain: sensor
|
||||
device_class: carbon_dioxide
|
||||
- domain: number
|
||||
device_class: carbon_dioxide
|
||||
|
||||
.number_or_entity_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
|
||||
.pm1_threshold_entity: &pm1_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm1
|
||||
- domain: number
|
||||
device_class: pm1
|
||||
|
||||
.number_or_entity_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
|
||||
.pm25_threshold_entity: &pm25_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm25
|
||||
- domain: number
|
||||
device_class: pm25
|
||||
|
||||
.unit_co: &unit_co
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "mg/m³"
|
||||
- "μg/m³"
|
||||
.pm4_threshold_entity: &pm4_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm4
|
||||
- domain: number
|
||||
device_class: pm4
|
||||
|
||||
.unit_ozone: &unit_ozone
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
.pm10_threshold_entity: &pm10_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm10
|
||||
- domain: number
|
||||
device_class: pm10
|
||||
|
||||
.unit_no2: &unit_no2
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
.ozone_threshold_entity: &ozone_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *ozone_units
|
||||
- domain: sensor
|
||||
device_class: ozone
|
||||
- domain: number
|
||||
device_class: ozone
|
||||
|
||||
.unit_no: &unit_no
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
.voc_threshold_entity: &voc_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *voc_units
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds
|
||||
|
||||
.unit_so2: &unit_so2
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
.voc_ratio_threshold_entity: &voc_ratio_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *voc_ratio_units
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds_parts
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds_parts
|
||||
|
||||
.unit_voc: &unit_voc
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "μg/m³"
|
||||
- "mg/m³"
|
||||
.no_threshold_entity: &no_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *no_units
|
||||
- domain: sensor
|
||||
device_class: nitrogen_monoxide
|
||||
- domain: number
|
||||
device_class: nitrogen_monoxide
|
||||
|
||||
.unit_voc_ratio: &unit_voc_ratio
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
.no2_threshold_entity: &no2_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *no2_units
|
||||
- domain: sensor
|
||||
device_class: nitrogen_dioxide
|
||||
- domain: number
|
||||
device_class: nitrogen_dioxide
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- above
|
||||
- below
|
||||
- between
|
||||
- outside
|
||||
translation_key: trigger_threshold_type
|
||||
.n2o_threshold_entity: &n2o_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: nitrous_oxide
|
||||
- domain: number
|
||||
device_class: nitrous_oxide
|
||||
|
||||
.so2_threshold_entity: &so2_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *so2_units
|
||||
- domain: sensor
|
||||
device_class: sulphur_dioxide
|
||||
- domain: number
|
||||
device_class: sulphur_dioxide
|
||||
|
||||
# --- Number anchors for single-unit pollutants ---
|
||||
|
||||
.co2_threshold_number: &co2_threshold_number
|
||||
mode: box
|
||||
unit_of_measurement: "ppm"
|
||||
|
||||
.ugm3_threshold_number: &ugm3_threshold_number
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
|
||||
# Binary sensor detected/cleared trigger fields
|
||||
.trigger_binary_fields: &trigger_binary_fields
|
||||
@@ -495,198 +276,342 @@ smoke_cleared:
|
||||
|
||||
# --- Numerical sensor triggers ---
|
||||
|
||||
# CO (multi-unit)
|
||||
co_changed:
|
||||
target: *target_co_sensor
|
||||
fields:
|
||||
above: *number_or_entity_co
|
||||
below: *number_or_entity_co
|
||||
unit: *unit_co
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *co_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *co_units
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *co_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *co_units
|
||||
|
||||
# CO2 (single-unit: ppm)
|
||||
co2_changed:
|
||||
target: *target_co2
|
||||
fields:
|
||||
above: *number_or_entity_co2
|
||||
below: *number_or_entity_co2
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *co2_threshold_entity
|
||||
mode: changed
|
||||
number: *co2_threshold_number
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *co2_threshold_entity
|
||||
mode: crossed
|
||||
number: *co2_threshold_number
|
||||
|
||||
# PM1 (single-unit: μg/m³)
|
||||
pm1_changed:
|
||||
target: *target_pm1
|
||||
fields:
|
||||
above: *number_or_entity_pm1
|
||||
below: *number_or_entity_pm1
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm1_threshold_entity
|
||||
mode: changed
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm1_threshold_entity
|
||||
mode: crossed
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
# PM2.5 (single-unit: μg/m³)
|
||||
pm25_changed:
|
||||
target: *target_pm25
|
||||
fields:
|
||||
above: *number_or_entity_pm25
|
||||
below: *number_or_entity_pm25
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm25_threshold_entity
|
||||
mode: changed
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm25_threshold_entity
|
||||
mode: crossed
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
# PM4 (single-unit: μg/m³)
|
||||
pm4_changed:
|
||||
target: *target_pm4
|
||||
fields:
|
||||
above: *number_or_entity_pm4
|
||||
below: *number_or_entity_pm4
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm4_threshold_entity
|
||||
mode: changed
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm4_threshold_entity
|
||||
mode: crossed
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
# PM10 (single-unit: μg/m³)
|
||||
pm10_changed:
|
||||
target: *target_pm10
|
||||
fields:
|
||||
above: *number_or_entity_pm10
|
||||
below: *number_or_entity_pm10
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm10_threshold_entity
|
||||
mode: changed
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *pm10_threshold_entity
|
||||
mode: crossed
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
# Ozone (multi-unit)
|
||||
ozone_changed:
|
||||
target: *target_ozone
|
||||
fields:
|
||||
above: *number_or_entity_ozone
|
||||
below: *number_or_entity_ozone
|
||||
unit: *unit_ozone
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *ozone_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *ozone_units
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *ozone_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *ozone_units
|
||||
|
||||
# VOC (multi-unit)
|
||||
voc_changed:
|
||||
target: *target_voc
|
||||
fields:
|
||||
above: *number_or_entity_voc
|
||||
below: *number_or_entity_voc
|
||||
unit: *unit_voc
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *voc_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *voc_units
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *voc_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *voc_units
|
||||
|
||||
# VOC ratio (multi-unit)
|
||||
voc_ratio_changed:
|
||||
target: *target_voc_ratio
|
||||
fields:
|
||||
above: *number_or_entity_voc_ratio
|
||||
below: *number_or_entity_voc_ratio
|
||||
unit: *unit_voc_ratio
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *voc_ratio_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *voc_ratio_units
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *voc_ratio_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *voc_ratio_units
|
||||
|
||||
# NO (multi-unit)
|
||||
no_changed:
|
||||
target: *target_no
|
||||
fields:
|
||||
above: *number_or_entity_no
|
||||
below: *number_or_entity_no
|
||||
unit: *unit_no
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *no_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *no_units
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *no_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *no_units
|
||||
|
||||
# NO2 (multi-unit)
|
||||
no2_changed:
|
||||
target: *target_no2
|
||||
fields:
|
||||
above: *number_or_entity_no2
|
||||
below: *number_or_entity_no2
|
||||
unit: *unit_no2
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *no2_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *no2_units
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *no2_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *no2_units
|
||||
|
||||
# N2O (single-unit: μg/m³)
|
||||
n2o_changed:
|
||||
target: *target_n2o
|
||||
fields:
|
||||
above: *number_or_entity_n2o
|
||||
below: *number_or_entity_n2o
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *n2o_threshold_entity
|
||||
mode: changed
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *n2o_threshold_entity
|
||||
mode: crossed
|
||||
number: *ugm3_threshold_number
|
||||
|
||||
# SO2 (multi-unit)
|
||||
so2_changed:
|
||||
target: *target_so2
|
||||
fields:
|
||||
above: *number_or_entity_so2
|
||||
below: *number_or_entity_so2
|
||||
unit: *unit_so2
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *so2_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *so2_units
|
||||
|
||||
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
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *so2_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *so2_units
|
||||
|
||||
@@ -87,7 +87,7 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.debug("Successfully connected to %s", user_input[CONF_IP_ADDRESS])
|
||||
|
||||
device_info = await airq.fetch_device_info()
|
||||
await self.async_set_unique_id(device_info["id"])
|
||||
await self.async_set_unique_id(device_info["id"], raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
_LOGGER.debug("Creating an entry for %s", device_info["name"])
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"incomplete_discovery": "The discovered air-Q device did not provide a device ID. Ensure the firmware is up to date."
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted alarms.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted alarms to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"trigger_behavior_name": "Trigger when"
|
||||
},
|
||||
"conditions": {
|
||||
"is_armed": {
|
||||
"description": "Tests if one or more alarms are armed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -20,7 +17,6 @@
|
||||
"description": "Tests if one or more alarms are armed in away mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -30,7 +26,6 @@
|
||||
"description": "Tests if one or more alarms are armed in home mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -40,7 +35,6 @@
|
||||
"description": "Tests if one or more alarms are armed in night mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -50,7 +44,6 @@
|
||||
"description": "Tests if one or more alarms are armed in vacation mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -60,7 +53,6 @@
|
||||
"description": "Tests if one or more alarms are disarmed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -70,7 +62,6 @@
|
||||
"description": "Tests if one or more alarms are triggered.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -173,7 +164,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Arm away"
|
||||
"name": "Arm alarm away"
|
||||
},
|
||||
"alarm_arm_custom_bypass": {
|
||||
"description": "Arms an alarm while allowing to bypass a custom area.",
|
||||
@@ -183,7 +174,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Arm with custom bypass"
|
||||
"name": "Arm alarm with custom bypass"
|
||||
},
|
||||
"alarm_arm_home": {
|
||||
"description": "Arms an alarm in the home mode.",
|
||||
@@ -193,7 +184,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Arm home"
|
||||
"name": "Arm alarm home"
|
||||
},
|
||||
"alarm_arm_night": {
|
||||
"description": "Arms an alarm in the night mode.",
|
||||
@@ -203,7 +194,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Arm night"
|
||||
"name": "Arm alarm night"
|
||||
},
|
||||
"alarm_arm_vacation": {
|
||||
"description": "Arms an alarm in the vacation mode.",
|
||||
@@ -213,7 +204,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Arm vacation"
|
||||
"name": "Arm alarm vacation"
|
||||
},
|
||||
"alarm_disarm": {
|
||||
"description": "Disarms an alarm.",
|
||||
@@ -223,7 +214,7 @@
|
||||
"name": "Code"
|
||||
}
|
||||
},
|
||||
"name": "Disarm"
|
||||
"name": "Disarm alarm"
|
||||
},
|
||||
"alarm_trigger": {
|
||||
"description": "Triggers an alarm manually.",
|
||||
@@ -233,7 +224,7 @@
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Trigger"
|
||||
"name": "Trigger alarm"
|
||||
}
|
||||
},
|
||||
"title": "Alarm control panel",
|
||||
@@ -242,7 +233,6 @@
|
||||
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -252,7 +242,6 @@
|
||||
"description": "Triggers after one or more alarms become armed in away mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -262,7 +251,6 @@
|
||||
"description": "Triggers after one or more alarms become armed in home mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -272,7 +260,6 @@
|
||||
"description": "Triggers after one or more alarms become armed in night mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -282,7 +269,6 @@
|
||||
"description": "Triggers after one or more alarms become armed in vacation mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -292,7 +278,6 @@
|
||||
"description": "Triggers after one or more alarms become disarmed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -302,7 +287,6 @@
|
||||
"description": "Triggers after one or more alarms become triggered.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -54,7 +54,16 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_LOGIN_DATA],
|
||||
)
|
||||
self.previous_devices: set[str] = set()
|
||||
device_registry = dr.async_get(hass)
|
||||
self.previous_devices: set[str] = {
|
||||
identifier
|
||||
for device in device_registry.devices.get_devices_for_config_entry_id(
|
||||
entry.entry_id
|
||||
)
|
||||
if device.entry_type != dr.DeviceEntryType.SERVICE
|
||||
for identifier_domain, identifier in device.identifiers
|
||||
if identifier_domain == DOMAIN
|
||||
}
|
||||
|
||||
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
||||
"""Update device data."""
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==13.0.1"]
|
||||
"requirements": ["aioamazondevices==13.3.2"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
from python_homeassistant_analytics import (
|
||||
Environment,
|
||||
HomeassistantAnalyticsClient,
|
||||
HomeassistantAnalyticsConnectionError,
|
||||
)
|
||||
@@ -38,7 +39,7 @@ async def async_setup_entry(
|
||||
client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass))
|
||||
|
||||
try:
|
||||
integrations = await client.get_integrations()
|
||||
integrations = await client.get_integrations(Environment.NEXT)
|
||||
except HomeassistantAnalyticsConnectionError as ex:
|
||||
raise ConfigEntryNotReady("Could not fetch integration list") from ex
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
_LOGGER.debug("Updating statistics for the first time")
|
||||
usage_sum = 0.0
|
||||
last_stats_time = None
|
||||
allow_update_last_stored_hour = False
|
||||
else:
|
||||
if not meter.readings or len(meter.readings) == 0:
|
||||
_LOGGER.debug("No recent usage statistics found, skipping update")
|
||||
@@ -107,6 +108,7 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
continue
|
||||
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
|
||||
_LOGGER.debug("Getting statistics at %s", start)
|
||||
stats: dict[str, list[Any]] = {}
|
||||
for end in (start + timedelta(seconds=1), None):
|
||||
stats = await get_instance(self.hass).async_add_executor_job(
|
||||
statistics_during_period,
|
||||
@@ -127,15 +129,28 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"Not found, trying to find oldest statistic after %s",
|
||||
start,
|
||||
)
|
||||
assert stats
|
||||
|
||||
def _safe_get_sum(records: list[Any]) -> float:
|
||||
if records and "sum" in records[0]:
|
||||
return float(records[0]["sum"])
|
||||
return 0.0
|
||||
if not stats or not stats.get(usage_statistic_id):
|
||||
_LOGGER.debug(
|
||||
"Could not find existing statistics during period lookup for %s, "
|
||||
"falling back to last stored statistic",
|
||||
usage_statistic_id,
|
||||
)
|
||||
allow_update_last_stored_hour = True
|
||||
last_records = last_stat[usage_statistic_id]
|
||||
usage_sum = float(last_records[0].get("sum") or 0.0)
|
||||
last_stats_time = last_records[0]["start"]
|
||||
else:
|
||||
allow_update_last_stored_hour = False
|
||||
records = stats[usage_statistic_id]
|
||||
|
||||
usage_sum = _safe_get_sum(stats.get(usage_statistic_id, []))
|
||||
last_stats_time = stats[usage_statistic_id][0]["start"]
|
||||
def _safe_get_sum(records: list[Any]) -> float:
|
||||
if records and "sum" in records[0]:
|
||||
return float(records[0]["sum"])
|
||||
return 0.0
|
||||
|
||||
usage_sum = _safe_get_sum(records)
|
||||
last_stats_time = records[0]["start"]
|
||||
|
||||
usage_statistics = []
|
||||
|
||||
@@ -148,7 +163,13 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
)
|
||||
continue
|
||||
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
|
||||
if last_stats_time is not None and start.timestamp() <= last_stats_time:
|
||||
if last_stats_time is not None and (
|
||||
start.timestamp() < last_stats_time
|
||||
or (
|
||||
start.timestamp() == last_stats_time
|
||||
and not allow_update_last_stored_hour
|
||||
)
|
||||
):
|
||||
continue
|
||||
usage_state = max(0, read["consumption"] / 1000)
|
||||
usage_sum = max(0, read["read"])
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyanglianwater"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyanglianwater==3.1.1"]
|
||||
"requirements": ["pyanglianwater==3.1.2"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["arcam"],
|
||||
"requirements": ["arcam-fmj==1.8.2"],
|
||||
"requirements": ["arcam-fmj==1.8.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -91,6 +91,7 @@ SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
|
||||
value_fn=lambda state: (
|
||||
vp.colorspace.name.lower()
|
||||
if (vp := state.get_incoming_video_parameters()) is not None
|
||||
and vp.colorspace is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
|
||||
@@ -137,5 +137,4 @@ async def async_pipeline_from_audio_stream(
|
||||
audio_settings=audio_settings or AudioSettings(),
|
||||
),
|
||||
)
|
||||
await pipeline_input.validate()
|
||||
await pipeline_input.execute()
|
||||
await pipeline_input.execute(validate=True)
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
"""Assist pipeline errors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .pipeline import PipelineStage
|
||||
|
||||
|
||||
class PipelineError(HomeAssistantError):
|
||||
"""Base class for pipeline errors."""
|
||||
@@ -55,3 +62,25 @@ class IntentRecognitionError(PipelineError):
|
||||
|
||||
class TextToSpeechError(PipelineError):
|
||||
"""Error in text-to-speech portion of pipeline."""
|
||||
|
||||
|
||||
class PipelineRunValidationError(PipelineError):
|
||||
"""Error when a pipeline run is not valid."""
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
"""Set error message."""
|
||||
super().__init__("validation-error", message)
|
||||
|
||||
|
||||
class InvalidPipelineStagesError(PipelineRunValidationError):
|
||||
"""Error when given an invalid combination of start/end stages."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start_stage: PipelineStage,
|
||||
end_stage: PipelineStage,
|
||||
) -> None:
|
||||
"""Set error message."""
|
||||
super().__init__(
|
||||
f"Invalid stage combination: start={start_stage}, end={end_stage}"
|
||||
)
|
||||
|
||||
@@ -73,8 +73,10 @@ from .const import (
|
||||
from .error import (
|
||||
DuplicateWakeUpDetectedError,
|
||||
IntentRecognitionError,
|
||||
InvalidPipelineStagesError,
|
||||
PipelineError,
|
||||
PipelineNotFound,
|
||||
PipelineRunValidationError,
|
||||
SpeechToTextError,
|
||||
TextToSpeechError,
|
||||
WakeWordDetectionAborted,
|
||||
@@ -492,24 +494,6 @@ PIPELINE_STAGE_ORDER = [
|
||||
]
|
||||
|
||||
|
||||
class PipelineRunValidationError(Exception):
|
||||
"""Error when a pipeline run is not valid."""
|
||||
|
||||
|
||||
class InvalidPipelineStagesError(PipelineRunValidationError):
|
||||
"""Error when given an invalid combination of start/end stages."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start_stage: PipelineStage,
|
||||
end_stage: PipelineStage,
|
||||
) -> None:
|
||||
"""Set error message."""
|
||||
super().__init__(
|
||||
f"Invalid stage combination: start={start_stage}, end={end_stage}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WakeWordSettings:
|
||||
"""Settings for wake word detection."""
|
||||
@@ -1680,26 +1664,39 @@ class PipelineInput:
|
||||
satellite_id: str | None = None
|
||||
"""Identifier of the satellite that is processing the input/output of the pipeline."""
|
||||
|
||||
async def execute(self) -> None:
|
||||
async def execute(self, validate: bool = False) -> None:
|
||||
"""Run pipeline."""
|
||||
validation_error: PipelineError | None = None
|
||||
if validate:
|
||||
try:
|
||||
await self.validate()
|
||||
except PipelineError as err:
|
||||
validation_error = err
|
||||
|
||||
self.run.start(
|
||||
conversation_id=self.session.conversation_id,
|
||||
device_id=self.device_id,
|
||||
satellite_id=self.satellite_id,
|
||||
)
|
||||
current_stage: PipelineStage | None = self.run.start_stage
|
||||
stt_audio_buffer: list[EnhancedAudioChunk] = []
|
||||
stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None
|
||||
|
||||
if self.stt_stream is not None:
|
||||
if self.run.audio_settings.needs_processor:
|
||||
# VAD/noise suppression/auto gain/volume
|
||||
stt_processed_stream = self.run.process_enhance_audio(self.stt_stream)
|
||||
else:
|
||||
# Volume multiplier only
|
||||
stt_processed_stream = self.run.process_volume_only(self.stt_stream)
|
||||
|
||||
try:
|
||||
if validation_error is not None:
|
||||
raise validation_error
|
||||
|
||||
stt_audio_buffer: list[EnhancedAudioChunk] = []
|
||||
stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None
|
||||
|
||||
if self.stt_stream is not None:
|
||||
if self.run.audio_settings.needs_processor:
|
||||
# VAD/noise suppression/auto gain/volume
|
||||
stt_processed_stream = self.run.process_enhance_audio(
|
||||
self.stt_stream
|
||||
)
|
||||
else:
|
||||
# Volume multiplier only
|
||||
stt_processed_stream = self.run.process_volume_only(self.stt_stream)
|
||||
|
||||
if current_stage == PipelineStage.WAKE_WORD:
|
||||
# wake-word-detection
|
||||
assert stt_processed_stream is not None
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted Assist satellites.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted Assist satellites to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"trigger_behavior_name": "Trigger when"
|
||||
},
|
||||
"conditions": {
|
||||
"is_idle": {
|
||||
"description": "Tests if one or more Assist satellites are idle.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -20,7 +17,6 @@
|
||||
"description": "Tests if one or more Assist satellites are listening.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -30,7 +26,6 @@
|
||||
"description": "Tests if one or more Assist satellites are processing.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -40,7 +35,6 @@
|
||||
"description": "Tests if one or more Assist satellites are responding.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -81,7 +75,7 @@
|
||||
},
|
||||
"services": {
|
||||
"announce": {
|
||||
"description": "Lets a satellite announce a message.",
|
||||
"description": "Lets an Assist satellite announce a message.",
|
||||
"fields": {
|
||||
"media_id": {
|
||||
"description": "The media ID to announce instead of using text-to-speech.",
|
||||
@@ -100,10 +94,10 @@
|
||||
"name": "Preannounce media ID"
|
||||
}
|
||||
},
|
||||
"name": "Announce"
|
||||
"name": "Announce on satellite"
|
||||
},
|
||||
"ask_question": {
|
||||
"description": "Asks a question and gets the user's response.",
|
||||
"description": "Lets an Assist satellite ask a question and get the user's response.",
|
||||
"fields": {
|
||||
"answers": {
|
||||
"description": "Possible answers to the question.",
|
||||
@@ -130,10 +124,10 @@
|
||||
"name": "Question media ID"
|
||||
}
|
||||
},
|
||||
"name": "Ask question"
|
||||
"name": "Ask question on satellite"
|
||||
},
|
||||
"start_conversation": {
|
||||
"description": "Starts a conversation from a satellite.",
|
||||
"description": "Starts a conversation from an Assist satellite.",
|
||||
"fields": {
|
||||
"extra_system_prompt": {
|
||||
"description": "Provide background information to the AI about the request.",
|
||||
@@ -156,46 +150,42 @@
|
||||
"name": "Message"
|
||||
}
|
||||
},
|
||||
"name": "Start conversation"
|
||||
"name": "Start conversation on satellite"
|
||||
}
|
||||
},
|
||||
"title": "Assist satellite",
|
||||
"triggers": {
|
||||
"idle": {
|
||||
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
|
||||
"description": "Triggers after one or more Assist satellites become idle after having processed a command.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite became idle"
|
||||
},
|
||||
"listening": {
|
||||
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
|
||||
"description": "Triggers after one or more Assist satellites start listening for a command from someone.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite started listening"
|
||||
},
|
||||
"processing": {
|
||||
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
|
||||
"description": "Triggers after one or more Assist satellites start processing a command after having heard it.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite started processing"
|
||||
},
|
||||
"responding": {
|
||||
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
|
||||
"description": "Triggers after one or more Assist satellites start responding to a command after having processed it, or start announcing something.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -626,7 +626,7 @@ def websocket_delete_all_refresh_tokens(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Handle delete all refresh tokens request."""
|
||||
current_refresh_token: RefreshToken
|
||||
current_refresh_token: RefreshToken | None = None
|
||||
remove_failed = False
|
||||
token_type = msg.get("token_type")
|
||||
delete_current_token = msg.get("delete_current_token")
|
||||
@@ -654,7 +654,7 @@ def websocket_delete_all_refresh_tokens(
|
||||
else:
|
||||
connection.send_result(msg["id"], {})
|
||||
|
||||
async def _delete_current_token_soon() -> None:
|
||||
async def _delete_current_token_soon(current_refresh_token: RefreshToken) -> None:
|
||||
"""Delete the current token after a delay.
|
||||
|
||||
We do not want to delete the current token immediately as it will
|
||||
@@ -675,13 +675,15 @@ def websocket_delete_all_refresh_tokens(
|
||||
# the token right away.
|
||||
hass.auth.async_remove_refresh_token(current_refresh_token)
|
||||
|
||||
if delete_current_token and (
|
||||
not limit_token_types or current_refresh_token.token_type == token_type
|
||||
if (
|
||||
delete_current_token
|
||||
and current_refresh_token
|
||||
and (not limit_token_types or current_refresh_token.token_type == token_type)
|
||||
):
|
||||
# Deleting the token will close the connection so we need
|
||||
# to do it with a delay in a tracked task to ensure it still
|
||||
# happens if Home Assistant is shutting down.
|
||||
hass.async_create_task(_delete_current_token_soon())
|
||||
hass.async_create_task(_delete_current_token_soon(current_refresh_token))
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
|
||||
@@ -118,10 +118,13 @@ SERVICE_TRIGGER = "trigger"
|
||||
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
|
||||
|
||||
_EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"air_quality",
|
||||
"alarm_control_panel",
|
||||
"assist_satellite",
|
||||
"battery",
|
||||
"calendar",
|
||||
"climate",
|
||||
"counter",
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"door",
|
||||
@@ -129,18 +132,27 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"garage_door",
|
||||
"gate",
|
||||
"humidifier",
|
||||
"humidity",
|
||||
"illuminance",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"moisture",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"person",
|
||||
"power",
|
||||
"schedule",
|
||||
"select",
|
||||
"siren",
|
||||
"switch",
|
||||
"temperature",
|
||||
"text",
|
||||
"timer",
|
||||
"vacuum",
|
||||
"valve",
|
||||
"water_heater",
|
||||
"window",
|
||||
}
|
||||
|
||||
@@ -148,8 +160,10 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"air_quality",
|
||||
"alarm_control_panel",
|
||||
"assist_satellite",
|
||||
"battery",
|
||||
"button",
|
||||
"climate",
|
||||
"counter",
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"door",
|
||||
@@ -164,6 +178,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"moisture",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"person",
|
||||
@@ -176,8 +191,10 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"switch",
|
||||
"temperature",
|
||||
"text",
|
||||
"todo",
|
||||
"update",
|
||||
"vacuum",
|
||||
"valve",
|
||||
"water_heater",
|
||||
"window",
|
||||
}
|
||||
|
||||
@@ -78,11 +78,11 @@
|
||||
"services": {
|
||||
"reload": {
|
||||
"description": "Reloads the automation configuration.",
|
||||
"name": "[%key:common::action::reload%]"
|
||||
"name": "Reload automations"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Toggles (enable / disable) an automation.",
|
||||
"name": "[%key:common::action::toggle%]"
|
||||
"name": "Toggle automation"
|
||||
},
|
||||
"trigger": {
|
||||
"description": "Triggers the actions of an automation.",
|
||||
@@ -92,7 +92,7 @@
|
||||
"name": "Skip conditions"
|
||||
}
|
||||
},
|
||||
"name": "Trigger"
|
||||
"name": "Trigger automation"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Disables an automation.",
|
||||
@@ -102,11 +102,11 @@
|
||||
"name": "Stop actions"
|
||||
}
|
||||
},
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
"name": "Turn off automation"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Enables an automation.",
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
"name": "Turn on automation"
|
||||
}
|
||||
},
|
||||
"title": "Automation"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==67"],
|
||||
"requirements": ["axis==68"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -74,6 +74,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackblazeConfigEntry) ->
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_bucket_name",
|
||||
) from err
|
||||
except exception.BadRequest as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="bad_request",
|
||||
translation_placeholders={"error_message": str(err)},
|
||||
) from err
|
||||
except (
|
||||
exception.B2ConnectionError,
|
||||
exception.B2RequestTimeout,
|
||||
|
||||
@@ -101,8 +101,7 @@ def handle_b2_errors[T](
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except B2Error as err:
|
||||
error_msg = f"Failed during {func.__name__}"
|
||||
raise BackupAgentError(error_msg) from err
|
||||
raise BackupAgentError(f"Failed during {func.__name__}: {err}") from err
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -170,8 +169,7 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
async def _cleanup_failed_upload(self, filename: str) -> None:
|
||||
"""Clean up a partially uploaded file after upload failure."""
|
||||
_LOGGER.warning(
|
||||
"Attempting to delete partially uploaded main backup file %s "
|
||||
"due to metadata upload failure",
|
||||
"Attempting to delete partially uploaded backup file %s",
|
||||
filename,
|
||||
)
|
||||
try:
|
||||
@@ -180,11 +178,10 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
)
|
||||
await self._hass.async_add_executor_job(uploaded_main_file_info.delete)
|
||||
except B2Error:
|
||||
_LOGGER.debug(
|
||||
"Failed to clean up partially uploaded main backup file %s. "
|
||||
"Manual intervention may be required to delete it from Backblaze B2",
|
||||
_LOGGER.warning(
|
||||
"Failed to clean up partially uploaded backup file %s;"
|
||||
" manual deletion from Backblaze B2 may be required",
|
||||
filename,
|
||||
exc_info=True,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
@@ -256,9 +253,10 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
prefixed_metadata_filename,
|
||||
)
|
||||
|
||||
upload_successful = False
|
||||
tar_uploaded = False
|
||||
try:
|
||||
await self._upload_backup_file(prefixed_tar_filename, open_stream, {})
|
||||
tar_uploaded = True
|
||||
_LOGGER.debug(
|
||||
"Main backup file upload finished for %s", prefixed_tar_filename
|
||||
)
|
||||
@@ -270,15 +268,14 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
_LOGGER.debug(
|
||||
"Metadata file upload finished for %s", prefixed_metadata_filename
|
||||
)
|
||||
upload_successful = True
|
||||
finally:
|
||||
if upload_successful:
|
||||
_LOGGER.debug("Backup upload complete: %s", prefixed_tar_filename)
|
||||
self._invalidate_caches(
|
||||
backup.backup_id, prefixed_tar_filename, prefixed_metadata_filename
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("Backup upload complete: %s", prefixed_tar_filename)
|
||||
self._invalidate_caches(
|
||||
backup.backup_id, prefixed_tar_filename, prefixed_metadata_filename
|
||||
)
|
||||
except B2Error:
|
||||
if tar_uploaded:
|
||||
await self._cleanup_failed_upload(prefixed_tar_filename)
|
||||
raise
|
||||
|
||||
def _upload_metadata_file_sync(
|
||||
self, metadata_content: bytes, filename: str
|
||||
|
||||
@@ -174,6 +174,14 @@ class BackblazeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"Backblaze B2 bucket '%s' does not exist", user_input[CONF_BUCKET]
|
||||
)
|
||||
errors[CONF_BUCKET] = "invalid_bucket_name"
|
||||
except exception.BadRequest as err:
|
||||
_LOGGER.error(
|
||||
"Backblaze B2 API rejected the request for Key ID '%s': %s",
|
||||
user_input[CONF_KEY_ID],
|
||||
err,
|
||||
)
|
||||
errors["base"] = "bad_request"
|
||||
placeholders["error_message"] = str(err)
|
||||
except (
|
||||
exception.B2ConnectionError,
|
||||
exception.B2RequestTimeout,
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["b2sdk"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["b2sdk==2.10.1"]
|
||||
"requirements": ["b2sdk==2.10.4"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"bad_request": "The Backblaze B2 API rejected the request: {error_message}",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_bucket_name": "[%key:component::backblaze_b2::exceptions::invalid_bucket_name::message%]",
|
||||
"invalid_capability": "[%key:component::backblaze_b2::exceptions::invalid_capability::message%]",
|
||||
@@ -60,6 +61,9 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"bad_request": {
|
||||
"message": "The Backblaze B2 API rejected the request: {error_message}"
|
||||
},
|
||||
"cannot_connect": {
|
||||
"message": "Cannot connect to endpoint"
|
||||
},
|
||||
|
||||
@@ -34,4 +34,4 @@ EXCLUDE_DATABASE_FROM_BACKUP = [
|
||||
"home-assistant_v2.db-wal",
|
||||
]
|
||||
|
||||
SECURETAR_CREATE_VERSION = 2
|
||||
SECURETAR_CREATE_VERSION = 3
|
||||
|
||||
@@ -12,7 +12,7 @@ import hashlib
|
||||
import io
|
||||
from itertools import chain
|
||||
import json
|
||||
from pathlib import Path, PurePath
|
||||
from pathlib import Path, PurePath, PureWindowsPath
|
||||
import shutil
|
||||
import sys
|
||||
import tarfile
|
||||
@@ -1957,7 +1957,10 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
suggested_filename: str,
|
||||
) -> WrittenBackup:
|
||||
"""Receive a backup."""
|
||||
temp_file = Path(self.temp_backup_dir, suggested_filename)
|
||||
safe_filename = PureWindowsPath(suggested_filename).name
|
||||
if not safe_filename or safe_filename == "..":
|
||||
safe_filename = "backup.tar"
|
||||
temp_file = Path(self.temp_backup_dir, safe_filename)
|
||||
|
||||
async_add_executor_job = self._hass.async_add_executor_job
|
||||
await async_add_executor_job(make_backup_dir, self.temp_backup_dir)
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["cronsim==2.7", "securetar==2026.2.0"],
|
||||
"requirements": ["cronsim==2.7", "securetar==2026.4.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ from securetar import (
|
||||
SecureTarFile,
|
||||
SecureTarReadError,
|
||||
SecureTarRootKeyContext,
|
||||
get_archive_max_ciphertext_size,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -431,7 +432,9 @@ class _CipherBackupStreamer:
|
||||
|
||||
def size(self) -> int:
|
||||
"""Return the maximum size of the decrypted or encrypted backup."""
|
||||
return self._backup.size + self._num_tar_files() * tarfile.RECORDSIZE
|
||||
return get_archive_max_ciphertext_size( # type: ignore[no-any-return]
|
||||
self._backup.size, SECURETAR_CREATE_VERSION, self._num_tar_files()
|
||||
)
|
||||
|
||||
def _num_tar_files(self) -> int:
|
||||
"""Return the number of inner tar files."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Integration for battery conditions."""
|
||||
"""Integration for battery triggers and conditions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -27,7 +26,6 @@ BATTERY_CHARGING_DOMAIN_SPECS = {
|
||||
}
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS = {
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
|
||||
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.BATTERY),
|
||||
}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
@@ -37,7 +35,7 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_not_charging": make_entity_state_condition(
|
||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
|
||||
),
|
||||
"percentage": make_entity_numerical_condition(
|
||||
"is_level": make_entity_numerical_condition(
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS, PERCENTAGE
|
||||
),
|
||||
}
|
||||
|
||||
@@ -14,24 +14,19 @@
|
||||
- all
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
unit_of_measurement: "%"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
domain:
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
translation_key: number_or_entity
|
||||
.battery_threshold_entity: &battery_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
device_class: battery
|
||||
- domain: number
|
||||
device_class: battery
|
||||
|
||||
.battery_threshold_number: &battery_threshold_number
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
|
||||
is_low: *condition_common
|
||||
|
||||
@@ -53,14 +48,17 @@ is_not_charging:
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
|
||||
percentage:
|
||||
is_level:
|
||||
target:
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: battery
|
||||
- domain: number
|
||||
device_class: battery
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *battery_threshold_entity
|
||||
mode: is
|
||||
number: *battery_threshold_number
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"is_charging": {
|
||||
"condition": "mdi:battery-charging"
|
||||
},
|
||||
"is_level": {
|
||||
"condition": "mdi:battery-unknown"
|
||||
},
|
||||
"is_low": {
|
||||
"condition": "mdi:battery-alert"
|
||||
},
|
||||
@@ -11,9 +14,26 @@
|
||||
},
|
||||
"is_not_low": {
|
||||
"condition": "mdi:battery"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"level_changed": {
|
||||
"trigger": "mdi:battery-unknown"
|
||||
},
|
||||
"percentage": {
|
||||
"condition": "mdi:battery-unknown"
|
||||
"level_crossed_threshold": {
|
||||
"trigger": "mdi:battery-alert"
|
||||
},
|
||||
"low": {
|
||||
"trigger": "mdi:battery-alert"
|
||||
},
|
||||
"not_low": {
|
||||
"trigger": "mdi:battery"
|
||||
},
|
||||
"started_charging": {
|
||||
"trigger": "mdi:battery-charging"
|
||||
},
|
||||
"stopped_charging": {
|
||||
"trigger": "mdi:battery"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted batteries.",
|
||||
"condition_behavior_name": "Behavior"
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_threshold_name": "Threshold type"
|
||||
},
|
||||
"conditions": {
|
||||
"is_charging": {
|
||||
"description": "Tests if one or more batteries are charging.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery is charging"
|
||||
},
|
||||
"is_level": {
|
||||
"description": "Tests the battery level of one or more batteries.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::battery::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery level"
|
||||
},
|
||||
"is_low": {
|
||||
"description": "Tests if one or more batteries are low.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -28,7 +40,6 @@
|
||||
"description": "Tests if one or more batteries are not charging.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -38,29 +49,10 @@
|
||||
"description": "Tests if one or more batteries are not low.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery is not low"
|
||||
},
|
||||
"percentage": {
|
||||
"description": "Tests the percentage of one or more batteries.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Require the percentage to be above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::battery::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Require the percentage to be below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "Battery percentage"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
@@ -70,12 +62,72 @@
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
"number": "Number"
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Battery"
|
||||
"title": "Battery",
|
||||
"triggers": {
|
||||
"level_changed": {
|
||||
"description": "Triggers after the battery level of one or more batteries changes.",
|
||||
"fields": {
|
||||
"threshold": {
|
||||
"name": "[%key:component::battery::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery level changed"
|
||||
},
|
||||
"level_crossed_threshold": {
|
||||
"description": "Triggers after the battery level of one or more batteries crosses a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "[%key:component::battery::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery level crossed threshold"
|
||||
},
|
||||
"low": {
|
||||
"description": "Triggers after one or more batteries become low.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery low"
|
||||
},
|
||||
"not_low": {
|
||||
"description": "Triggers after one or more batteries are no longer low.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery not low"
|
||||
},
|
||||
"started_charging": {
|
||||
"description": "Triggers after one or more batteries start charging.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery started charging"
|
||||
},
|
||||
"stopped_charging": {
|
||||
"description": "Triggers after one or more batteries stop charging.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery stopped charging"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
54
homeassistant/components/battery/trigger.py
Normal file
54
homeassistant/components/battery/trigger.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Provides triggers for batteries."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
make_entity_numerical_state_changed_trigger,
|
||||
make_entity_numerical_state_crossed_threshold_trigger,
|
||||
make_entity_target_state_trigger,
|
||||
)
|
||||
|
||||
BATTERY_LOW_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.BATTERY),
|
||||
}
|
||||
|
||||
BATTERY_CHARGING_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
BINARY_SENSOR_DOMAIN: DomainSpec(
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING
|
||||
),
|
||||
}
|
||||
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
|
||||
}
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"low": make_entity_target_state_trigger(BATTERY_LOW_DOMAIN_SPECS, STATE_ON),
|
||||
"not_low": make_entity_target_state_trigger(BATTERY_LOW_DOMAIN_SPECS, STATE_OFF),
|
||||
"started_charging": make_entity_target_state_trigger(
|
||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON
|
||||
),
|
||||
"stopped_charging": make_entity_target_state_trigger(
|
||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
|
||||
),
|
||||
"level_changed": make_entity_numerical_state_changed_trigger(
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS, valid_unit="%"
|
||||
),
|
||||
"level_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS, valid_unit="%"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for batteries."""
|
||||
return TRIGGERS
|
||||
83
homeassistant/components/battery/triggers.yaml
Normal file
83
homeassistant/components/battery/triggers.yaml
Normal file
@@ -0,0 +1,83 @@
|
||||
.trigger_common_fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.battery_threshold_entity: &battery_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: number
|
||||
device_class: battery
|
||||
- domain: sensor
|
||||
device_class: battery
|
||||
|
||||
.battery_threshold_number: &battery_threshold_number
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
|
||||
.trigger_target_battery: &trigger_target_battery
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: battery
|
||||
|
||||
.trigger_target_charging: &trigger_target_charging
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: battery_charging
|
||||
|
||||
.trigger_target_percentage: &trigger_target_percentage
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: battery
|
||||
|
||||
low:
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
target: *trigger_target_battery
|
||||
|
||||
not_low:
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
target: *trigger_target_battery
|
||||
|
||||
started_charging:
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
target: *trigger_target_charging
|
||||
|
||||
stopped_charging:
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
target: *trigger_target_charging
|
||||
|
||||
level_changed:
|
||||
target: *trigger_target_percentage
|
||||
fields:
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *battery_threshold_entity
|
||||
mode: changed
|
||||
number: *battery_threshold_number
|
||||
|
||||
level_crossed_threshold:
|
||||
target: *trigger_target_percentage
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *battery_threshold_entity
|
||||
mode: crossed
|
||||
number: *battery_threshold_number
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyblu==2.0.5"],
|
||||
"requirements": ["pyblu==2.0.6"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_musc._tcp.local."
|
||||
|
||||
41
homeassistant/components/bmw_connected_drive/__init__.py
Normal file
41
homeassistant/components/bmw_connected_drive/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""The BMW Connected Drive integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
DOMAIN = "bmw_connected_drive"
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up BMW Connected Drive from a config entry."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="integration_removed",
|
||||
translation_placeholders={
|
||||
"entries": "/config/integrations/integration/bmw_connected_drive",
|
||||
"custom_component_url": "https://github.com/kvanbiesen/bmw-cardata-ha",
|
||||
},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
@@ -0,0 +1,9 @@
|
||||
"""The BMW Connected Drive integration config flow."""
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
class BMWConnectedDriveConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for BMW Connected Drive."""
|
||||
10
homeassistant/components/bmw_connected_drive/manifest.json
Normal file
10
homeassistant/components/bmw_connected_drive/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "BMW Connected Drive",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"issues": {
|
||||
"integration_removed": {
|
||||
"description": "The BMW Connected Drive integration has been removed from Home Assistant.\n\nIn September 2025, BMW blocked third-party access to their servers by adding additional security measures. For EU-registered cars, a community-developed [custom component]({custom_component_url}) using BMW's CarData API is available as an alternative.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing BMW Connected Drive integration entries]({entries}).",
|
||||
"title": "The BMW Connected Drive integration has been removed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ from bsblan import (
|
||||
BSBLAN,
|
||||
BSBLANAuthError,
|
||||
BSBLANConnectionError,
|
||||
BSBLANError,
|
||||
HotWaterConfig,
|
||||
HotWaterSchedule,
|
||||
HotWaterState,
|
||||
@@ -50,7 +51,7 @@ class BSBLanFastData:
|
||||
|
||||
state: State
|
||||
sensor: Sensor
|
||||
dhw: HotWaterState
|
||||
dhw: HotWaterState | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -111,7 +112,6 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
|
||||
# This reduces response time significantly (~0.2s per parameter)
|
||||
state = await self.client.state(include=STATE_INCLUDE)
|
||||
sensor = await self.client.sensor(include=SENSOR_INCLUDE)
|
||||
dhw = await self.client.hot_water_state(include=DHW_STATE_INCLUDE)
|
||||
|
||||
except BSBLANAuthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
@@ -126,6 +126,19 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
|
||||
translation_placeholders={"host": host},
|
||||
) from err
|
||||
|
||||
# Fetch DHW state separately - device may not support hot water
|
||||
dhw: HotWaterState | None = None
|
||||
try:
|
||||
dhw = await self.client.hot_water_state(include=DHW_STATE_INCLUDE)
|
||||
except BSBLANError:
|
||||
# Preserve last known DHW state if available (entity may depend on it)
|
||||
if self.data:
|
||||
dhw = self.data.dhw
|
||||
LOGGER.debug(
|
||||
"DHW (Domestic Hot Water) state not available on device at %s",
|
||||
self.config_entry.data[CONF_HOST],
|
||||
)
|
||||
|
||||
return BSBLanFastData(
|
||||
state=state,
|
||||
sensor=sensor,
|
||||
@@ -159,13 +172,6 @@ class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
|
||||
dhw_config = await self.client.hot_water_config(include=DHW_CONFIG_INCLUDE)
|
||||
dhw_schedule = await self.client.hot_water_schedule()
|
||||
|
||||
except AttributeError:
|
||||
# Device does not support DHW functionality
|
||||
LOGGER.debug(
|
||||
"DHW (Domestic Hot Water) not available on device at %s",
|
||||
self.config_entry.data[CONF_HOST],
|
||||
)
|
||||
return BSBLanSlowData()
|
||||
except (BSBLANConnectionError, BSBLANAuthError) as err:
|
||||
# If config update fails, keep existing data
|
||||
LOGGER.debug(
|
||||
@@ -177,6 +183,13 @@ class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
|
||||
return self.data
|
||||
# First fetch failed, return empty data
|
||||
return BSBLanSlowData()
|
||||
except BSBLANError, AttributeError:
|
||||
# Device does not support DHW functionality
|
||||
LOGGER.debug(
|
||||
"DHW (Domestic Hot Water) not available on device at %s",
|
||||
self.config_entry.data[CONF_HOST],
|
||||
)
|
||||
return BSBLanSlowData()
|
||||
|
||||
return BSBLanSlowData(
|
||||
dhw_config=dhw_config,
|
||||
|
||||
@@ -22,7 +22,9 @@ async def async_get_config_entry_diagnostics(
|
||||
"fast_coordinator_data": {
|
||||
"state": data.fast_coordinator.data.state.model_dump(),
|
||||
"sensor": data.fast_coordinator.data.sensor.model_dump(),
|
||||
"dhw": data.fast_coordinator.data.dhw.model_dump(),
|
||||
"dhw": data.fast_coordinator.data.dhw.model_dump()
|
||||
if data.fast_coordinator.data.dhw
|
||||
else None,
|
||||
},
|
||||
"static": data.static.model_dump() if data.static is not None else None,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
DeviceInfo,
|
||||
@@ -10,7 +13,7 @@ from homeassistant.helpers.device_registry import (
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import BSBLanData
|
||||
from .const import DOMAIN
|
||||
from .const import DEFAULT_PORT, DOMAIN
|
||||
from .coordinator import BSBLanCoordinator, BSBLanFastCoordinator, BSBLanSlowCoordinator
|
||||
|
||||
|
||||
@@ -22,7 +25,8 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
|
||||
def __init__(self, coordinator: _T, data: BSBLanData) -> None:
|
||||
"""Initialize BSBLan entity with device info."""
|
||||
super().__init__(coordinator)
|
||||
host = coordinator.config_entry.data["host"]
|
||||
host = coordinator.config_entry.data[CONF_HOST]
|
||||
port = coordinator.config_entry.data.get(CONF_PORT, DEFAULT_PORT)
|
||||
mac = data.device.MAC
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, mac)},
|
||||
@@ -44,7 +48,7 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
|
||||
else None
|
||||
),
|
||||
sw_version=data.device.version,
|
||||
configuration_url=f"http://{host}",
|
||||
configuration_url=str(URL.build(scheme="http", host=host, port=port)),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-bsblan==5.1.2"],
|
||||
"requirements": ["python-bsblan==5.1.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from bsblan import BSBLANError, SetHotWaterParam
|
||||
from bsblan import BSBLANError, HotWaterState, SetHotWaterParam
|
||||
|
||||
from homeassistant.components.water_heater import (
|
||||
STATE_ECO,
|
||||
@@ -46,8 +46,10 @@ async def async_setup_entry(
|
||||
data = entry.runtime_data
|
||||
|
||||
# Only create water heater entity if DHW (Domestic Hot Water) is available
|
||||
# Check if we have any DHW-related data indicating water heater support
|
||||
dhw_data = data.fast_coordinator.data.dhw
|
||||
if dhw_data is None:
|
||||
# Device does not support DHW, skip water heater setup
|
||||
return
|
||||
if (
|
||||
dhw_data.operating_mode is None
|
||||
and dhw_data.nominal_setpoint is None
|
||||
@@ -107,11 +109,21 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
|
||||
else:
|
||||
self._attr_max_temp = 65.0 # Default maximum
|
||||
|
||||
@property
|
||||
def _dhw(self) -> HotWaterState:
|
||||
"""Return DHW state data.
|
||||
|
||||
This entity is only created when DHW data is available.
|
||||
"""
|
||||
dhw = self.coordinator.data.dhw
|
||||
assert dhw is not None
|
||||
return dhw
|
||||
|
||||
@property
|
||||
def current_operation(self) -> str | None:
|
||||
"""Return current operation."""
|
||||
if (
|
||||
operating_mode := self.coordinator.data.dhw.operating_mode
|
||||
operating_mode := self._dhw.operating_mode
|
||||
) is None or operating_mode.value is None:
|
||||
return None
|
||||
return BSBLAN_TO_HA_OPERATION_MODE.get(operating_mode.value)
|
||||
@@ -119,16 +131,14 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
if (
|
||||
current_temp := self.coordinator.data.dhw.dhw_actual_value_top_temperature
|
||||
) is None:
|
||||
if (current_temp := self._dhw.dhw_actual_value_top_temperature) is None:
|
||||
return None
|
||||
return current_temp.value
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
if (target_temp := self.coordinator.data.dhw.nominal_setpoint) is None:
|
||||
if (target_temp := self._dhw.nominal_setpoint) is None:
|
||||
return None
|
||||
return target_temp.value
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
},
|
||||
"services": {
|
||||
"press": {
|
||||
"description": "Presses a button entity.",
|
||||
"name": "Press"
|
||||
"description": "Presses a button.",
|
||||
"name": "Press button"
|
||||
}
|
||||
},
|
||||
"title": "Button",
|
||||
|
||||
16
homeassistant/components/calendar/condition.py
Normal file
16
homeassistant/components/calendar/condition.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Provides conditions for calendars."""
|
||||
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_event_active": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the calendar conditions."""
|
||||
return CONDITIONS
|
||||
14
homeassistant/components/calendar/conditions.yaml
Normal file
14
homeassistant/components/calendar/conditions.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
is_event_active:
|
||||
target:
|
||||
entity:
|
||||
- domain: calendar
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_event_active": {
|
||||
"condition": "mdi:calendar-check"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:calendar",
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if"
|
||||
},
|
||||
"conditions": {
|
||||
"is_event_active": {
|
||||
"description": "Tests if one or more calendars have an active event.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::calendar::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Calendar event is active"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::calendar::title%]",
|
||||
@@ -46,6 +60,12 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_offset_type": {
|
||||
"options": {
|
||||
"after": "After",
|
||||
@@ -90,7 +110,7 @@
|
||||
"name": "Summary"
|
||||
}
|
||||
},
|
||||
"name": "Create event"
|
||||
"name": "Create calendar event"
|
||||
},
|
||||
"get_events": {
|
||||
"description": "Retrieves events on a calendar within a time range.",
|
||||
@@ -108,7 +128,7 @@
|
||||
"name": "Start time"
|
||||
}
|
||||
},
|
||||
"name": "Get events"
|
||||
"name": "Get calendar events"
|
||||
}
|
||||
},
|
||||
"title": "Calendar",
|
||||
|
||||
@@ -51,11 +51,11 @@
|
||||
"services": {
|
||||
"disable_motion_detection": {
|
||||
"description": "Disables the motion detection of a camera.",
|
||||
"name": "Disable motion detection"
|
||||
"name": "Disable camera motion detection"
|
||||
},
|
||||
"enable_motion_detection": {
|
||||
"description": "Enables the motion detection of a camera.",
|
||||
"name": "Enable motion detection"
|
||||
"name": "Enable camera motion detection"
|
||||
},
|
||||
"play_stream": {
|
||||
"description": "Plays a camera stream on a supported media player.",
|
||||
@@ -69,7 +69,7 @@
|
||||
"name": "Media player"
|
||||
}
|
||||
},
|
||||
"name": "Play stream"
|
||||
"name": "Play camera stream"
|
||||
},
|
||||
"record": {
|
||||
"description": "Creates a recording of a live camera feed.",
|
||||
@@ -87,7 +87,7 @@
|
||||
"name": "Lookback"
|
||||
}
|
||||
},
|
||||
"name": "Record"
|
||||
"name": "Record camera feed"
|
||||
},
|
||||
"snapshot": {
|
||||
"description": "Takes a snapshot from a camera.",
|
||||
@@ -97,15 +97,15 @@
|
||||
"name": "Filename"
|
||||
}
|
||||
},
|
||||
"name": "Take snapshot"
|
||||
"name": "Take camera snapshot"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Turns off a camera.",
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
"name": "Turn off camera"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Turns on a camera.",
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
"name": "Turn on camera"
|
||||
}
|
||||
},
|
||||
"title": "Camera"
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.LIGHT]
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:
|
||||
|
||||
51
homeassistant/components/casper_glow/binary_sensor.py
Normal file
51
homeassistant/components/casper_glow/binary_sensor.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Casper Glow integration binary sensor platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
from .entity import CasperGlowEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: CasperGlowConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the binary sensor platform for Casper Glow."""
|
||||
async_add_entities([CasperGlowPausedBinarySensor(entry.runtime_data)])
|
||||
|
||||
|
||||
class CasperGlowPausedBinarySensor(CasperGlowEntity, BinarySensorEntity):
|
||||
"""Binary sensor indicating whether the Casper Glow dimming is paused."""
|
||||
|
||||
_attr_translation_key = "paused"
|
||||
|
||||
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
|
||||
"""Initialize the paused binary sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_paused"
|
||||
if coordinator.device.state.is_paused is not None:
|
||||
self._attr_is_on = coordinator.device.state.is_paused
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register state update callback when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self._device.register_callback(self._async_handle_state_update)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_handle_state_update(self, state: GlowState) -> None:
|
||||
"""Handle a state update from the device."""
|
||||
if state.is_paused is not None:
|
||||
self._attr_is_on = state.is_paused
|
||||
self.async_write_ha_state()
|
||||
73
homeassistant/components/casper_glow/button.py
Normal file
73
homeassistant/components/casper_glow/button.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Casper Glow integration button platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pycasperglow import CasperGlow
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
from .entity import CasperGlowEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class CasperGlowButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describe a Casper Glow button entity."""
|
||||
|
||||
press_fn: Callable[[CasperGlow], Awaitable[None]]
|
||||
|
||||
|
||||
BUTTON_DESCRIPTIONS: tuple[CasperGlowButtonEntityDescription, ...] = (
|
||||
CasperGlowButtonEntityDescription(
|
||||
key="pause",
|
||||
translation_key="pause",
|
||||
press_fn=lambda device: device.pause(),
|
||||
),
|
||||
CasperGlowButtonEntityDescription(
|
||||
key="resume",
|
||||
translation_key="resume",
|
||||
press_fn=lambda device: device.resume(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: CasperGlowConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the button platform for Casper Glow."""
|
||||
async_add_entities(
|
||||
CasperGlowButton(entry.runtime_data, description)
|
||||
for description in BUTTON_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class CasperGlowButton(CasperGlowEntity, ButtonEntity):
|
||||
"""A Casper Glow button entity."""
|
||||
|
||||
entity_description: CasperGlowButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: CasperGlowCoordinator,
|
||||
description: CasperGlowButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a Casper Glow button."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = (
|
||||
f"{format_mac(coordinator.device.address)}_{description.key}"
|
||||
)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
await self._async_command(self.entity_description.press_fn(self._device))
|
||||
17
homeassistant/components/casper_glow/icons.json
Normal file
17
homeassistant/components/casper_glow/icons.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"paused": {
|
||||
"default": "mdi:timer-pause"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"pause": {
|
||||
"default": "mdi:pause"
|
||||
},
|
||||
"resume": {
|
||||
"default": "mdi:play"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pycasperglow"],
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pycasperglow==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: Bluetooth device with no authentication credentials.
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
@@ -53,15 +55,9 @@ rules:
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: No entity translations needed.
|
||||
exception-translations:
|
||||
status: exempt
|
||||
comment: No custom services that raise exceptions.
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: No icon translations needed.
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
@@ -26,6 +26,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"paused": {
|
||||
"name": "Dimming paused"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"pause": {
|
||||
"name": "Pause dimming"
|
||||
},
|
||||
"resume": {
|
||||
"name": "Resume dimming"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"communication_error": {
|
||||
"message": "An error occurred while communicating with the Casper Glow: {error}"
|
||||
|
||||
@@ -30,6 +30,7 @@ class ChessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
client = ChessComClient(session=session)
|
||||
try:
|
||||
user = await client.get_player(user_input[CONF_USERNAME])
|
||||
await client.get_player_stats(user_input[CONF_USERNAME])
|
||||
except NotFoundError:
|
||||
errors["base"] = "player_not_found"
|
||||
except Exception:
|
||||
@@ -38,7 +39,9 @@ class ChessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
await self.async_set_unique_id(str(user.player_id))
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=user.name, data=user_input)
|
||||
return self.async_create_entry(
|
||||
title=user.name or user.username, data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
|
||||
@@ -1,20 +1,68 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"chess960_daily_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess960_daily_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess960_daily_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess960_daily_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_blitz_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_blitz_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_blitz_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess_blitz_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_bullet_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_bullet_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_bullet_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess_bullet_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_daily_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_daily_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_daily_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess_daily_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_rapid_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_rapid_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"chess_rapid_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"chess_rapid_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"followers": {
|
||||
"default": "mdi:account-multiple"
|
||||
},
|
||||
"total_daily_draw": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"total_daily_lost": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"total_daily_won": {
|
||||
"default": "mdi:chess-pawn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from chess_com_api import PlayerStats
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
@@ -24,7 +27,14 @@ class ChessEntityDescription(SensorEntityDescription):
|
||||
value_fn: Callable[[ChessData], float]
|
||||
|
||||
|
||||
SENSORS: tuple[ChessEntityDescription, ...] = (
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class ChessModeEntityDescription(SensorEntityDescription):
|
||||
"""Sensor description for a Chess.com game mode."""
|
||||
|
||||
value_fn: Callable[[dict[str, Any]], float]
|
||||
|
||||
|
||||
PLAYER_SENSORS: tuple[ChessEntityDescription, ...] = (
|
||||
ChessEntityDescription(
|
||||
key="followers",
|
||||
translation_key="followers",
|
||||
@@ -33,35 +43,46 @@ SENSORS: tuple[ChessEntityDescription, ...] = (
|
||||
value_fn=lambda state: state.player.followers,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
ChessEntityDescription(
|
||||
key="chess_daily_rating",
|
||||
translation_key="chess_daily_rating",
|
||||
)
|
||||
|
||||
GAME_MODE_SENSORS: tuple[ChessModeEntityDescription, ...] = (
|
||||
ChessModeEntityDescription(
|
||||
key="rating",
|
||||
translation_key="rating",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda state: state.stats.chess_daily["last"]["rating"],
|
||||
value_fn=lambda mode: mode["last"]["rating"],
|
||||
),
|
||||
ChessEntityDescription(
|
||||
key="total_daily_won",
|
||||
translation_key="total_daily_won",
|
||||
ChessModeEntityDescription(
|
||||
key="won",
|
||||
translation_key="won",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda state: state.stats.chess_daily["record"]["win"],
|
||||
value_fn=lambda mode: mode["record"]["win"],
|
||||
),
|
||||
ChessEntityDescription(
|
||||
key="total_daily_lost",
|
||||
translation_key="total_daily_lost",
|
||||
ChessModeEntityDescription(
|
||||
key="lost",
|
||||
translation_key="lost",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda state: state.stats.chess_daily["record"]["loss"],
|
||||
value_fn=lambda mode: mode["record"]["loss"],
|
||||
),
|
||||
ChessEntityDescription(
|
||||
key="total_daily_draw",
|
||||
translation_key="total_daily_draw",
|
||||
ChessModeEntityDescription(
|
||||
key="draw",
|
||||
translation_key="draw",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda state: state.stats.chess_daily["record"]["draw"],
|
||||
value_fn=lambda mode: mode["record"]["draw"],
|
||||
),
|
||||
)
|
||||
|
||||
GAME_MODES: dict[str, Callable[[PlayerStats], dict[str, Any] | None]] = {
|
||||
"chess_daily": lambda stats: stats.chess_daily,
|
||||
"chess_rapid": lambda stats: stats.chess_rapid,
|
||||
"chess_bullet": lambda stats: stats.chess_bullet,
|
||||
"chess_blitz": lambda stats: stats.chess_blitz,
|
||||
"chess960_daily": lambda stats: stats.chess960_daily,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -71,13 +92,22 @@ async def async_setup_entry(
|
||||
"""Initialize the entries."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
ChessPlayerSensor(coordinator, description) for description in SENSORS
|
||||
)
|
||||
entities: list[SensorEntity] = [
|
||||
ChessPlayerSensor(coordinator, description) for description in PLAYER_SENSORS
|
||||
]
|
||||
|
||||
for game_mode, stats_fn in GAME_MODES.items():
|
||||
if stats_fn(coordinator.data.stats) is not None:
|
||||
entities.extend(
|
||||
ChessGameModeSensor(coordinator, description, game_mode, stats_fn)
|
||||
for description in GAME_MODE_SENSORS
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ChessPlayerSensor(ChessEntity, SensorEntity):
|
||||
"""Chess.com sensor."""
|
||||
"""Chess.com player sensor."""
|
||||
|
||||
entity_description: ChessEntityDescription
|
||||
|
||||
@@ -95,3 +125,33 @@ class ChessPlayerSensor(ChessEntity, SensorEntity):
|
||||
def native_value(self) -> float:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
|
||||
class ChessGameModeSensor(ChessEntity, SensorEntity):
|
||||
"""Chess.com game mode sensor."""
|
||||
|
||||
entity_description: ChessModeEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ChessCoordinator,
|
||||
description: ChessModeEntityDescription,
|
||||
game_mode: str,
|
||||
stats_fn: Callable[[PlayerStats], dict[str, Any] | None],
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._stats_fn = stats_fn
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.config_entry.unique_id}.{game_mode}.{description.key}"
|
||||
)
|
||||
self._attr_translation_key = f"{game_mode}_{description.translation_key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the state of the sensor."""
|
||||
mode_data = self._stats_fn(self.coordinator.data.stats)
|
||||
if TYPE_CHECKING:
|
||||
assert mode_data is not None
|
||||
return self.entity_description.value_fn(mode_data)
|
||||
|
||||
@@ -23,24 +23,84 @@
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"chess960_daily_draw": {
|
||||
"name": "Total daily Chess960 games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess960_daily_lost": {
|
||||
"name": "Total daily Chess960 games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess960_daily_rating": {
|
||||
"name": "Daily Chess960 rating"
|
||||
},
|
||||
"chess960_daily_won": {
|
||||
"name": "Total daily Chess960 games won",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_blitz_draw": {
|
||||
"name": "Total blitz chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_blitz_lost": {
|
||||
"name": "Total blitz chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_blitz_rating": {
|
||||
"name": "Blitz chess rating"
|
||||
},
|
||||
"chess_blitz_won": {
|
||||
"name": "Total blitz chess games won",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_bullet_draw": {
|
||||
"name": "Total bullet chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_bullet_lost": {
|
||||
"name": "Total bullet chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_bullet_rating": {
|
||||
"name": "Bullet chess rating"
|
||||
},
|
||||
"chess_bullet_won": {
|
||||
"name": "Total bullet chess games won",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_daily_draw": {
|
||||
"name": "Total daily chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_daily_lost": {
|
||||
"name": "Total daily chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_daily_rating": {
|
||||
"name": "Daily chess rating"
|
||||
},
|
||||
"chess_daily_won": {
|
||||
"name": "Total daily chess games won",
|
||||
"unit_of_measurement": "games"
|
||||
},
|
||||
"chess_rapid_draw": {
|
||||
"name": "Total rapid chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_rapid_lost": {
|
||||
"name": "Total rapid chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"chess_rapid_rating": {
|
||||
"name": "Rapid chess rating"
|
||||
},
|
||||
"chess_rapid_won": {
|
||||
"name": "Total rapid chess games won",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::chess_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"followers": {
|
||||
"name": "Followers",
|
||||
"unit_of_measurement": "followers"
|
||||
},
|
||||
"total_daily_draw": {
|
||||
"name": "Total chess games drawn",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::total_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"total_daily_lost": {
|
||||
"name": "Total chess games lost",
|
||||
"unit_of_measurement": "[%key:component::chess_com::entity::sensor::total_daily_won::unit_of_measurement%]"
|
||||
},
|
||||
"total_daily_won": {
|
||||
"name": "Total chess games won",
|
||||
"unit_of_measurement": "games"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
"""Provides conditions for climates."""
|
||||
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
|
||||
Condition,
|
||||
ConditionConfig,
|
||||
EntityConditionBase,
|
||||
EntityNumericalConditionWithUnitBase,
|
||||
make_entity_numerical_condition,
|
||||
make_entity_state_condition,
|
||||
@@ -13,12 +21,42 @@ from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
||||
|
||||
CONF_HVAC_MODE = "hvac_mode"
|
||||
|
||||
_HVAC_MODE_CONDITION_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): {
|
||||
vol.Required(CONF_HVAC_MODE): vol.All(
|
||||
cv.ensure_list, vol.Length(min=1), [vol.Coerce(HVACMode)]
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ClimateHVACModeCondition(EntityConditionBase):
|
||||
"""Condition for climate HVAC mode."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
_schema = _HVAC_MODE_CONDITION_SCHEMA
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
||||
"""Initialize the HVAC mode condition."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
self._hvac_modes: set[str] = set(config.options[CONF_HVAC_MODE])
|
||||
|
||||
def is_valid_state(self, entity_state: State) -> bool:
|
||||
"""Check if the state matches any of the expected HVAC modes."""
|
||||
return entity_state.state in self._hvac_modes
|
||||
|
||||
|
||||
class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
|
||||
"""Mixin for climate target temperature conditions with unit conversion."""
|
||||
|
||||
_base_unit = UnitOfTemperature.CELSIUS
|
||||
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_unit_converter = TemperatureConverter
|
||||
|
||||
def _get_entity_unit(self, entity_state: State) -> str | None:
|
||||
@@ -28,6 +66,7 @@ class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_hvac_mode": ClimateHVACModeCondition,
|
||||
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
|
||||
"is_on": make_entity_state_condition(
|
||||
DOMAIN,
|
||||
@@ -50,7 +89,7 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
|
||||
),
|
||||
"target_humidity": make_entity_numerical_condition(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"target_temperature": ClimateTargetTemperatureCondition,
|
||||
|
||||
@@ -13,58 +13,31 @@
|
||||
- all
|
||||
- any
|
||||
|
||||
.number_or_entity_humidity: &number_or_entity_humidity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
device_class: humidity
|
||||
- domain: number
|
||||
device_class: humidity
|
||||
translation_key: number_or_entity
|
||||
.humidity_threshold_entity: &humidity_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
device_class: humidity
|
||||
- domain: number
|
||||
device_class: humidity
|
||||
|
||||
.number_or_entity_temperature: &number_or_entity_temperature
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "°C"
|
||||
- "°F"
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
translation_key: number_or_entity
|
||||
.humidity_threshold_number: &humidity_threshold_number
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
|
||||
.condition_unit_temperature: &condition_unit_temperature
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "°C"
|
||||
- "°F"
|
||||
.temperature_units: &temperature_units
|
||||
- "°C"
|
||||
- "°F"
|
||||
|
||||
.temperature_threshold_entity: &temperature_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *temperature_units
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
@@ -72,17 +45,43 @@ is_cooling: *condition_common
|
||||
is_drying: *condition_common
|
||||
is_heating: *condition_common
|
||||
|
||||
is_hvac_mode:
|
||||
target: *condition_climate_target
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
hvac_mode:
|
||||
context:
|
||||
filter_target: target
|
||||
required: true
|
||||
selector:
|
||||
state:
|
||||
hide_states:
|
||||
- unavailable
|
||||
- unknown
|
||||
multiple: true
|
||||
|
||||
target_humidity:
|
||||
target: *condition_climate_target
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_humidity
|
||||
below: *number_or_entity_humidity
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *humidity_threshold_entity
|
||||
mode: is
|
||||
number: *humidity_threshold_number
|
||||
|
||||
target_temperature:
|
||||
target: *condition_climate_target
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_temperature
|
||||
below: *number_or_entity_temperature
|
||||
unit: *condition_unit_temperature
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *temperature_threshold_entity
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *temperature_units
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
"is_heating": {
|
||||
"condition": "mdi:fire"
|
||||
},
|
||||
"is_hvac_mode": {
|
||||
"condition": "mdi:thermostat"
|
||||
},
|
||||
"is_off": {
|
||||
"condition": "mdi:power-off"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,8 @@ set_preset_mode:
|
||||
required: true
|
||||
example: "away"
|
||||
selector:
|
||||
text:
|
||||
state:
|
||||
attribute: preset_mode
|
||||
|
||||
set_temperature:
|
||||
target:
|
||||
@@ -55,16 +56,10 @@ set_temperature:
|
||||
mode: box
|
||||
hvac_mode:
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "off"
|
||||
- "auto"
|
||||
- "cool"
|
||||
- "dry"
|
||||
- "fan_only"
|
||||
- "heat_cool"
|
||||
- "heat"
|
||||
translation_key: hvac_mode
|
||||
state:
|
||||
hide_states:
|
||||
- unavailable
|
||||
- unknown
|
||||
set_humidity:
|
||||
target:
|
||||
entity:
|
||||
@@ -91,7 +86,8 @@ set_fan_mode:
|
||||
required: true
|
||||
example: "low"
|
||||
selector:
|
||||
text:
|
||||
state:
|
||||
attribute: fan_mode
|
||||
|
||||
set_hvac_mode:
|
||||
target:
|
||||
@@ -115,7 +111,8 @@ set_swing_mode:
|
||||
required: true
|
||||
example: "on"
|
||||
selector:
|
||||
text:
|
||||
state:
|
||||
attribute: swing_mode
|
||||
|
||||
set_swing_horizontal_mode:
|
||||
target:
|
||||
@@ -128,7 +125,8 @@ set_swing_horizontal_mode:
|
||||
required: true
|
||||
example: "on"
|
||||
selector:
|
||||
text:
|
||||
state:
|
||||
attribute: swing_horizontal_mode
|
||||
|
||||
turn_on:
|
||||
target:
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted climate-control devices.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted climates to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_threshold_name": "Threshold type"
|
||||
},
|
||||
"conditions": {
|
||||
"is_cooling": {
|
||||
"description": "Tests if one or more climate-control devices are cooling.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -20,7 +19,6 @@
|
||||
"description": "Tests if one or more climate-control devices are drying.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -30,17 +28,28 @@
|
||||
"description": "Tests if one or more climate-control devices are heating.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device is heating"
|
||||
},
|
||||
"is_hvac_mode": {
|
||||
"description": "Tests if one or more climate-control devices are set to a specific HVAC mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
},
|
||||
"hvac_mode": {
|
||||
"description": "The HVAC modes to test for.",
|
||||
"name": "Modes"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device HVAC mode"
|
||||
},
|
||||
"is_off": {
|
||||
"description": "Tests if one or more climate-control devices are off.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -50,7 +59,6 @@
|
||||
"description": "Tests if one or more climate-control devices are on.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -59,17 +67,11 @@
|
||||
"target_humidity": {
|
||||
"description": "Tests the humidity setpoint of one or more climate-control devices.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Require the target humidity to be above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Require the target humidity to be below this value.",
|
||||
"name": "Below"
|
||||
"threshold": {
|
||||
"name": "[%key:component::climate::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target humidity"
|
||||
@@ -77,21 +79,11 @@
|
||||
"target_temperature": {
|
||||
"description": "Tests the temperature setpoint of one or more climate-control devices.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Require the target temperature to be above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "Require the target temperature to be below this value.",
|
||||
"name": "Below"
|
||||
},
|
||||
"unit": {
|
||||
"description": "All values will be converted to this unit when evaluating the condition.",
|
||||
"name": "Unit of measurement"
|
||||
"threshold": {
|
||||
"name": "[%key:component::climate::common::condition_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target temperature"
|
||||
@@ -281,102 +273,77 @@
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"hvac_mode": {
|
||||
"options": {
|
||||
"auto": "[%key:common::state::auto%]",
|
||||
"cool": "Cool",
|
||||
"dry": "Dry",
|
||||
"fan_only": "Fan only",
|
||||
"heat": "Heat",
|
||||
"heat_cool": "Heat/cool",
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
"number": "Number"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
},
|
||||
"trigger_threshold_type": {
|
||||
"options": {
|
||||
"above": "Above a value",
|
||||
"below": "Below a value",
|
||||
"between": "In a range",
|
||||
"outside": "Outside a range"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"set_fan_mode": {
|
||||
"description": "Sets fan operation mode.",
|
||||
"description": "Sets the fan mode of a climate-control device.",
|
||||
"fields": {
|
||||
"fan_mode": {
|
||||
"description": "Fan operation mode.",
|
||||
"name": "Fan mode"
|
||||
}
|
||||
},
|
||||
"name": "Set fan mode"
|
||||
"name": "Set climate-control device fan mode"
|
||||
},
|
||||
"set_humidity": {
|
||||
"description": "Sets target humidity.",
|
||||
"description": "Sets the target humidity of a climate-control device.",
|
||||
"fields": {
|
||||
"humidity": {
|
||||
"description": "Target humidity.",
|
||||
"name": "Humidity"
|
||||
}
|
||||
},
|
||||
"name": "Set target humidity"
|
||||
"name": "Set climate-control device target humidity"
|
||||
},
|
||||
"set_hvac_mode": {
|
||||
"description": "Sets HVAC operation mode.",
|
||||
"description": "Sets the HVAC mode of a climate-control device.",
|
||||
"fields": {
|
||||
"hvac_mode": {
|
||||
"description": "HVAC operation mode.",
|
||||
"name": "HVAC mode"
|
||||
}
|
||||
},
|
||||
"name": "Set HVAC mode"
|
||||
"name": "Set climate-control device HVAC mode"
|
||||
},
|
||||
"set_preset_mode": {
|
||||
"description": "Sets preset mode.",
|
||||
"description": "Sets the preset mode of a climate-control device.",
|
||||
"fields": {
|
||||
"preset_mode": {
|
||||
"description": "Preset mode.",
|
||||
"name": "Preset mode"
|
||||
}
|
||||
},
|
||||
"name": "Set preset mode"
|
||||
"name": "Set climate-control device preset mode"
|
||||
},
|
||||
"set_swing_horizontal_mode": {
|
||||
"description": "Sets horizontal swing operation mode.",
|
||||
"description": "Sets the horizontal swing mode of a climate-control device.",
|
||||
"fields": {
|
||||
"swing_horizontal_mode": {
|
||||
"description": "Horizontal swing operation mode.",
|
||||
"name": "Horizontal swing mode"
|
||||
}
|
||||
},
|
||||
"name": "Set horizontal swing mode"
|
||||
"name": "Set climate-control device horizontal swing mode"
|
||||
},
|
||||
"set_swing_mode": {
|
||||
"description": "Sets swing operation mode.",
|
||||
"description": "Sets the swing mode of a climate-control device.",
|
||||
"fields": {
|
||||
"swing_mode": {
|
||||
"description": "Swing operation mode.",
|
||||
"name": "Swing mode"
|
||||
}
|
||||
},
|
||||
"name": "Set swing mode"
|
||||
"name": "Set climate-control device swing mode"
|
||||
},
|
||||
"set_temperature": {
|
||||
"description": "Sets the temperature setpoint.",
|
||||
"description": "Sets the target temperature of a climate-control device.",
|
||||
"fields": {
|
||||
"hvac_mode": {
|
||||
"description": "HVAC operation mode.",
|
||||
@@ -395,19 +362,19 @@
|
||||
"name": "Target temperature"
|
||||
}
|
||||
},
|
||||
"name": "Set target temperature"
|
||||
"name": "Set climate-control device target temperature"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Toggles climate device, from on to off, or off to on.",
|
||||
"name": "[%key:common::action::toggle%]"
|
||||
"description": "Toggles a climate-control device on/off.",
|
||||
"name": "Toggle climate-control device"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Turns climate device off.",
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
"description": "Turns off a climate-control device.",
|
||||
"name": "Turn off climate-control device"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Turns climate device on.",
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
"description": "Turns on a climate-control device.",
|
||||
"name": "Turn on climate-control device"
|
||||
}
|
||||
},
|
||||
"title": "Climate",
|
||||
@@ -416,7 +383,6 @@
|
||||
"description": "Triggers after the mode of one or more climate-control devices changes.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
},
|
||||
"hvac_mode": {
|
||||
@@ -430,7 +396,6 @@
|
||||
"description": "Triggers after one or more climate-control devices start cooling.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -440,7 +405,6 @@
|
||||
"description": "Triggers after one or more climate-control devices start drying.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -450,7 +414,6 @@
|
||||
"description": "Triggers after one or more climate-control devices start heating.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -459,13 +422,8 @@
|
||||
"target_humidity_changed": {
|
||||
"description": "Triggers after the humidity setpoint of one or more climate-control devices changes.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Trigger when the target humidity is above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Trigger when the target humidity is below this value.",
|
||||
"name": "Below"
|
||||
"threshold": {
|
||||
"name": "[%key:component::climate::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target humidity changed"
|
||||
@@ -474,20 +432,10 @@
|
||||
"description": "Triggers after the humidity setpoint of one or more climate-control devices crosses a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
},
|
||||
"lower_limit": {
|
||||
"description": "Lower threshold limit.",
|
||||
"name": "Lower threshold"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "Type of threshold crossing to trigger on.",
|
||||
"name": "Threshold type"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "Upper threshold limit.",
|
||||
"name": "Upper threshold"
|
||||
"threshold": {
|
||||
"name": "[%key:component::climate::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target humidity crossed threshold"
|
||||
@@ -495,17 +443,8 @@
|
||||
"target_temperature_changed": {
|
||||
"description": "Triggers after the temperature setpoint of one or more climate-control devices changes.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Trigger when the target temperature is above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Trigger when the target temperature is below this value.",
|
||||
"name": "Below"
|
||||
},
|
||||
"unit": {
|
||||
"description": "All values will be converted to this unit when evaluating the trigger.",
|
||||
"name": "Unit of measurement"
|
||||
"threshold": {
|
||||
"name": "[%key:component::climate::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target temperature changed"
|
||||
@@ -514,24 +453,10 @@
|
||||
"description": "Triggers after the temperature setpoint of one or more climate-control devices crosses a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
},
|
||||
"lower_limit": {
|
||||
"description": "Lower threshold limit.",
|
||||
"name": "Lower threshold"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "Type of threshold crossing to trigger on.",
|
||||
"name": "Threshold type"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::climate::triggers::target_temperature_changed::fields::unit::description%]",
|
||||
"name": "[%key:component::climate::triggers::target_temperature_changed::fields::unit::name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "Upper threshold limit.",
|
||||
"name": "Upper threshold"
|
||||
"threshold": {
|
||||
"name": "[%key:component::climate::common::trigger_threshold_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target temperature crossed threshold"
|
||||
@@ -540,7 +465,6 @@
|
||||
"description": "Triggers after one or more climate-control devices turn off.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -550,7 +474,6 @@
|
||||
"description": "Triggers after one or more climate-control devices turn on, regardless of the mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::climate::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::climate::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||
@@ -52,7 +52,7 @@ class _ClimateTargetTemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitB
|
||||
"""Mixin for climate target temperature triggers with unit conversion."""
|
||||
|
||||
_base_unit = UnitOfTemperature.CELSIUS
|
||||
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_unit_converter = TemperatureConverter
|
||||
|
||||
def _get_entity_unit(self, state: State) -> str | None:
|
||||
@@ -84,11 +84,11 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.DRYING
|
||||
),
|
||||
"target_humidity_changed": make_entity_numerical_state_changed_trigger(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"target_temperature_changed": ClimateTargetTemperatureChangedTrigger,
|
||||
|
||||
@@ -14,70 +14,31 @@
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity_humidity: &number_or_entity_humidity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
device_class: humidity
|
||||
- domain: number
|
||||
device_class: humidity
|
||||
translation_key: number_or_entity
|
||||
.humidity_threshold_entity: &humidity_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
device_class: humidity
|
||||
- domain: number
|
||||
device_class: humidity
|
||||
|
||||
.number_or_entity_temperature: &number_or_entity_temperature
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "°C"
|
||||
- "°F"
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
translation_key: number_or_entity
|
||||
.humidity_threshold_number: &humidity_threshold_number
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
|
||||
.trigger_unit_temperature: &trigger_unit_temperature
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "°C"
|
||||
- "°F"
|
||||
.temperature_units: &temperature_units
|
||||
- "°C"
|
||||
- "°F"
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- above
|
||||
- below
|
||||
- between
|
||||
- outside
|
||||
translation_key: trigger_threshold_type
|
||||
.temperature_threshold_entity: &temperature_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: *temperature_units
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
|
||||
started_cooling: *trigger_common
|
||||
started_drying: *trigger_common
|
||||
@@ -103,29 +64,49 @@ hvac_mode_changed:
|
||||
target_humidity_changed:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
above: *number_or_entity_humidity
|
||||
below: *number_or_entity_humidity
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *humidity_threshold_entity
|
||||
mode: changed
|
||||
number: *humidity_threshold_number
|
||||
|
||||
target_humidity_crossed_threshold:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_humidity
|
||||
upper_limit: *number_or_entity_humidity
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *humidity_threshold_entity
|
||||
mode: crossed
|
||||
number: *humidity_threshold_number
|
||||
|
||||
target_temperature_changed:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
above: *number_or_entity_temperature
|
||||
below: *number_or_entity_temperature
|
||||
unit: *trigger_unit_temperature
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *temperature_threshold_entity
|
||||
mode: changed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *temperature_units
|
||||
|
||||
target_temperature_crossed_threshold:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_temperature
|
||||
upper_limit: *number_or_entity_temperature
|
||||
unit: *trigger_unit_temperature
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity: *temperature_threshold_entity
|
||||
mode: crossed
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: *temperature_units
|
||||
|
||||
@@ -75,11 +75,11 @@
|
||||
"services": {
|
||||
"remote_connect": {
|
||||
"description": "Makes the instance UI accessible from outside of the local network by enabling your Home Assistant Cloud connection.",
|
||||
"name": "Enable remote access"
|
||||
"name": "Enable Home Assistant Cloud remote access"
|
||||
},
|
||||
"remote_disconnect": {
|
||||
"description": "Disconnects the instance UI from Home Assistant Cloud. This disables access to it from outside your local network.",
|
||||
"name": "Disable remote access"
|
||||
"name": "Disable Home Assistant Cloud remote access"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
|
||||
@@ -112,7 +112,7 @@ class ComelitAlarmEntity(
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if alarm is available."""
|
||||
if self._area.human_status in [AlarmAreaState.ANOMALY, AlarmAreaState.UNKNOWN]:
|
||||
if self._area.human_status == AlarmAreaState.UNKNOWN:
|
||||
return False
|
||||
return super().available
|
||||
|
||||
@@ -151,7 +151,7 @@ class ComelitAlarmEntity(
|
||||
if code != str(self.coordinator.api.device_pin):
|
||||
return
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[DISABLE]
|
||||
self._area.index, ALARM_ACTIONS[DISABLE], self._area.anomaly
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.DISARMED, ALARM_AREA_ARMED_STATUS[DISABLE]
|
||||
@@ -160,7 +160,7 @@ class ComelitAlarmEntity(
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[AWAY]
|
||||
self._area.index, ALARM_ACTIONS[AWAY], self._area.anomaly
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[AWAY]
|
||||
@@ -169,7 +169,7 @@ class ComelitAlarmEntity(
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[HOME]
|
||||
self._area.index, ALARM_ACTIONS[HOME], self._area.anomaly
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[HOME_P1]
|
||||
@@ -178,7 +178,7 @@ class ComelitAlarmEntity(
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[NIGHT]
|
||||
self._area.index, ALARM_ACTIONS[NIGHT], self._area.anomaly
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT]
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==2.0.1"]
|
||||
"requirements": ["aiocomelit==2.0.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.3.3"]
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.3.24"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"services": {
|
||||
"process": {
|
||||
"description": "Launches a conversation from a transcribed text.",
|
||||
"description": "Sends text to a conversation agent for processing.",
|
||||
"fields": {
|
||||
"agent_id": {
|
||||
"description": "Conversation agent to process your request. The conversation agent is the brains of your assistant. It processes the incoming text commands.",
|
||||
@@ -25,10 +25,10 @@
|
||||
"name": "Text"
|
||||
}
|
||||
},
|
||||
"name": "Process"
|
||||
"name": "Process conversation"
|
||||
},
|
||||
"reload": {
|
||||
"description": "Reloads the intent configuration.",
|
||||
"description": "Reloads the intent configuration of conversation agents.",
|
||||
"fields": {
|
||||
"agent_id": {
|
||||
"description": "Conversation agent to reload.",
|
||||
@@ -39,7 +39,7 @@
|
||||
"name": "[%key:common::config_flow::data::language%]"
|
||||
}
|
||||
},
|
||||
"name": "[%key:common::action::reload%]"
|
||||
"name": "Reload conversation agents"
|
||||
}
|
||||
},
|
||||
"title": "Conversation"
|
||||
|
||||
15
homeassistant/components/counter/condition.py
Normal file
15
homeassistant/components/counter/condition.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""Provides conditions for counters."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition, make_entity_numerical_condition
|
||||
|
||||
DOMAIN = "counter"
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_value": make_entity_numerical_condition(DOMAIN),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the conditions for counters."""
|
||||
return CONDITIONS
|
||||
25
homeassistant/components/counter/conditions.yaml
Normal file
25
homeassistant/components/counter/conditions.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
is_value:
|
||||
target:
|
||||
entity:
|
||||
- domain: counter
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
numeric_threshold:
|
||||
entity:
|
||||
- domain: counter
|
||||
- domain: input_number
|
||||
- domain: number
|
||||
mode: is
|
||||
number:
|
||||
mode: box
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_value": {
|
||||
"condition": "mdi:counter"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"decrement": {
|
||||
"service": "mdi:numeric-negative-1"
|
||||
@@ -12,5 +17,22 @@
|
||||
"set_value": {
|
||||
"service": "mdi:counter"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"decremented": {
|
||||
"trigger": "mdi:numeric-negative-1"
|
||||
},
|
||||
"incremented": {
|
||||
"trigger": "mdi:numeric-positive-1"
|
||||
},
|
||||
"maximum_reached": {
|
||||
"trigger": "mdi:sort-numeric-ascending-variant"
|
||||
},
|
||||
"minimum_reached": {
|
||||
"trigger": "mdi:sort-numeric-descending-variant"
|
||||
},
|
||||
"reset": {
|
||||
"trigger": "mdi:refresh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_name": "Trigger when"
|
||||
},
|
||||
"conditions": {
|
||||
"is_value": {
|
||||
"description": "Tests the value of one or more counters.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "Condition passes if"
|
||||
},
|
||||
"threshold": {
|
||||
"name": "Threshold type"
|
||||
}
|
||||
},
|
||||
"name": "Counter value"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::counter::title%]",
|
||||
@@ -25,29 +42,81 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"decrement": {
|
||||
"description": "Decrements a counter by its step size.",
|
||||
"name": "Decrement"
|
||||
"name": "Decrement counter"
|
||||
},
|
||||
"increment": {
|
||||
"description": "Increments a counter by its step size.",
|
||||
"name": "Increment"
|
||||
"name": "Increment counter"
|
||||
},
|
||||
"reset": {
|
||||
"description": "Resets a counter to its initial value.",
|
||||
"name": "Reset"
|
||||
"name": "Reset counter"
|
||||
},
|
||||
"set_value": {
|
||||
"description": "Sets the counter to a specific value.",
|
||||
"description": "Sets a counter to a specific value.",
|
||||
"fields": {
|
||||
"value": {
|
||||
"description": "The new counter value the entity should be set to.",
|
||||
"name": "Value"
|
||||
}
|
||||
},
|
||||
"name": "Set"
|
||||
"name": "Set counter value"
|
||||
}
|
||||
},
|
||||
"title": "Counter"
|
||||
"title": "Counter",
|
||||
"triggers": {
|
||||
"decremented": {
|
||||
"description": "Triggers after one or more counters decrement.",
|
||||
"name": "Counter decremented"
|
||||
},
|
||||
"incremented": {
|
||||
"description": "Triggers after one or more counters increment.",
|
||||
"name": "Counter incremented"
|
||||
},
|
||||
"maximum_reached": {
|
||||
"description": "Triggers after one or more counters reach their maximum value.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::counter::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Counter reached maximum"
|
||||
},
|
||||
"minimum_reached": {
|
||||
"description": "Triggers after one or more counters reach their minimum value.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::counter::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Counter reached minimum"
|
||||
},
|
||||
"reset": {
|
||||
"description": "Triggers after one or more counters are reset.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::counter::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Counter reset"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
113
homeassistant/components/counter/trigger.py
Normal file
113
homeassistant/components/counter/trigger.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Provides triggers for counters."""
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_MAXIMUM,
|
||||
CONF_MINIMUM,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
||||
EntityTriggerBase,
|
||||
Trigger,
|
||||
)
|
||||
|
||||
from . import CONF_INITIAL, DOMAIN
|
||||
|
||||
|
||||
def _is_integer_state(state: State) -> bool:
|
||||
"""Return True if the state's value can be interpreted as an integer."""
|
||||
try:
|
||||
int(state.state)
|
||||
except TypeError, ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class CounterBaseIntegerTrigger(EntityTriggerBase):
|
||||
"""Base trigger for valid counter integer states."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
_schema = ENTITY_STATE_TRIGGER_SCHEMA
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state is valid."""
|
||||
return _is_integer_state(state)
|
||||
|
||||
|
||||
class CounterDecrementedTrigger(CounterBaseIntegerTrigger):
|
||||
"""Trigger for when a counter is decremented."""
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and the state has changed."""
|
||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return False
|
||||
return int(from_state.state) > int(to_state.state)
|
||||
|
||||
|
||||
class CounterIncrementedTrigger(CounterBaseIntegerTrigger):
|
||||
"""Trigger for when a counter is incremented."""
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and the state has changed."""
|
||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return False
|
||||
return int(from_state.state) < int(to_state.state)
|
||||
|
||||
|
||||
class CounterValueBaseTrigger(EntityTriggerBase):
|
||||
"""Base trigger for counter value changes."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and the state has changed."""
|
||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return False
|
||||
return from_state.state != to_state.state
|
||||
|
||||
|
||||
class CounterMaxReachedTrigger(CounterValueBaseTrigger):
|
||||
"""Trigger for when a counter reaches its maximum value."""
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state matches the expected state(s)."""
|
||||
if (max_value := state.attributes.get(CONF_MAXIMUM)) is None:
|
||||
return False
|
||||
return state.state == str(max_value)
|
||||
|
||||
|
||||
class CounterMinReachedTrigger(CounterValueBaseTrigger):
|
||||
"""Trigger for when a counter reaches its minimum value."""
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state matches the expected state(s)."""
|
||||
if (min_value := state.attributes.get(CONF_MINIMUM)) is None:
|
||||
return False
|
||||
return state.state == str(min_value)
|
||||
|
||||
|
||||
class CounterResetTrigger(CounterValueBaseTrigger):
|
||||
"""Trigger for reset of counter entities."""
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state matches the expected state(s)."""
|
||||
if (init_state := state.attributes.get(CONF_INITIAL)) is None:
|
||||
return False
|
||||
return state.state == str(init_state)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"decremented": CounterDecrementedTrigger,
|
||||
"incremented": CounterIncrementedTrigger,
|
||||
"maximum_reached": CounterMaxReachedTrigger,
|
||||
"minimum_reached": CounterMinReachedTrigger,
|
||||
"reset": CounterResetTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for counters."""
|
||||
return TRIGGERS
|
||||
27
homeassistant/components/counter/triggers.yaml
Normal file
27
homeassistant/components/counter/triggers.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: counter
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
incremented:
|
||||
target:
|
||||
entity:
|
||||
domain: counter
|
||||
decremented:
|
||||
target:
|
||||
entity:
|
||||
domain: counter
|
||||
maximum_reached: *trigger_common
|
||||
minimum_reached: *trigger_common
|
||||
reset: *trigger_common
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Provides conditions for covers."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.condition import Condition, EntityConditionBase
|
||||
@@ -8,9 +10,11 @@ from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
|
||||
from .models import CoverDomainSpec
|
||||
|
||||
|
||||
class CoverConditionBase(EntityConditionBase[CoverDomainSpec]):
|
||||
class CoverConditionBase(EntityConditionBase):
|
||||
"""Base condition for cover state checks."""
|
||||
|
||||
_domain_specs: Mapping[str, CoverDomainSpec]
|
||||
|
||||
def is_valid_state(self, entity_state: State) -> bool:
|
||||
"""Check if the state matches the expected cover state."""
|
||||
domain_spec = self._domain_specs[entity_state.domain]
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted covers.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted covers to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"trigger_behavior_name": "Trigger when"
|
||||
},
|
||||
"conditions": {
|
||||
"awning_is_closed": {
|
||||
"description": "Tests if one or more awnings are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -20,7 +17,6 @@
|
||||
"description": "Tests if one or more awnings are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -30,7 +26,6 @@
|
||||
"description": "Tests if one or more blinds are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -40,7 +35,6 @@
|
||||
"description": "Tests if one or more blinds are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -50,7 +44,6 @@
|
||||
"description": "Tests if one or more curtains are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -60,7 +53,6 @@
|
||||
"description": "Tests if one or more curtains are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -70,7 +62,6 @@
|
||||
"description": "Tests if one or more shades are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -80,7 +71,6 @@
|
||||
"description": "Tests if one or more shades are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -90,7 +80,6 @@
|
||||
"description": "Tests if one or more shutters are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -100,7 +89,6 @@
|
||||
"description": "Tests if one or more shutters are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -208,19 +196,19 @@
|
||||
"services": {
|
||||
"close_cover": {
|
||||
"description": "Closes a cover.",
|
||||
"name": "[%key:common::action::close%]"
|
||||
"name": "Close cover"
|
||||
},
|
||||
"close_cover_tilt": {
|
||||
"description": "Tilts a cover to close.",
|
||||
"name": "Close tilt"
|
||||
"name": "Close cover tilt"
|
||||
},
|
||||
"open_cover": {
|
||||
"description": "Opens a cover.",
|
||||
"name": "[%key:common::action::open%]"
|
||||
"name": "Open cover"
|
||||
},
|
||||
"open_cover_tilt": {
|
||||
"description": "Tilts a cover open.",
|
||||
"name": "Open tilt"
|
||||
"name": "Open cover tilt"
|
||||
},
|
||||
"set_cover_position": {
|
||||
"description": "Moves a cover to a specific position.",
|
||||
@@ -230,7 +218,7 @@
|
||||
"name": "Position"
|
||||
}
|
||||
},
|
||||
"name": "Set position"
|
||||
"name": "Set cover position"
|
||||
},
|
||||
"set_cover_tilt_position": {
|
||||
"description": "Moves a cover tilt to a specific position.",
|
||||
@@ -240,23 +228,23 @@
|
||||
"name": "Tilt position"
|
||||
}
|
||||
},
|
||||
"name": "Set tilt position"
|
||||
"name": "Set cover tilt position"
|
||||
},
|
||||
"stop_cover": {
|
||||
"description": "Stops the cover movement.",
|
||||
"name": "[%key:common::action::stop%]"
|
||||
"description": "Stops a cover's movement.",
|
||||
"name": "Stop cover"
|
||||
},
|
||||
"stop_cover_tilt": {
|
||||
"description": "Stops a tilting cover movement.",
|
||||
"name": "Stop tilt"
|
||||
"name": "Stop cover tilt"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Toggles a cover open/closed.",
|
||||
"name": "[%key:common::action::toggle%]"
|
||||
"name": "Toggle cover"
|
||||
},
|
||||
"toggle_cover_tilt": {
|
||||
"description": "Toggles a cover tilt open/closed.",
|
||||
"name": "Toggle tilt"
|
||||
"name": "Toggle cover tilt"
|
||||
}
|
||||
},
|
||||
"title": "Cover",
|
||||
@@ -265,7 +253,6 @@
|
||||
"description": "Triggers after one or more awnings close.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -275,7 +262,6 @@
|
||||
"description": "Triggers after one or more awnings open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -285,7 +271,6 @@
|
||||
"description": "Triggers after one or more blinds close.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -295,7 +280,6 @@
|
||||
"description": "Triggers after one or more blinds open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -305,7 +289,6 @@
|
||||
"description": "Triggers after one or more curtains close.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -315,7 +298,6 @@
|
||||
"description": "Triggers after one or more curtains open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -325,7 +307,6 @@
|
||||
"description": "Triggers after one or more shades close.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -335,7 +316,6 @@
|
||||
"description": "Triggers after one or more shades open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -345,7 +325,6 @@
|
||||
"description": "Triggers after one or more shutters close.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
@@ -355,7 +334,6 @@
|
||||
"description": "Triggers after one or more shutters open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::cover::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::cover::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Provides triggers for covers."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.trigger import EntityTriggerBase, Trigger
|
||||
@@ -8,9 +10,11 @@ from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
|
||||
from .models import CoverDomainSpec
|
||||
|
||||
|
||||
class CoverTriggerBase(EntityTriggerBase[CoverDomainSpec]):
|
||||
class CoverTriggerBase(EntityTriggerBase):
|
||||
"""Base trigger for cover state changes."""
|
||||
|
||||
_domain_specs: Mapping[str, CoverDomainSpec]
|
||||
|
||||
def _get_value(self, state: State) -> str | bool | None:
|
||||
"""Extract the relevant value from state based on domain spec."""
|
||||
domain_spec = self._domain_specs[state.domain]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"services": {
|
||||
"set_value": {
|
||||
"description": "Sets the date.",
|
||||
"description": "Sets the value of a date.",
|
||||
"fields": {
|
||||
"date": {
|
||||
"description": "The date to set.",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"services": {
|
||||
"set_value": {
|
||||
"description": "Sets the date/time for a datetime entity.",
|
||||
"description": "Sets the value of a date/time.",
|
||||
"fields": {
|
||||
"datetime": {
|
||||
"description": "The date/time to set. The time zone of the Home Assistant instance is assumed.",
|
||||
|
||||
@@ -1 +1,91 @@
|
||||
"""The decora_wifi component."""
|
||||
"""The Leviton Decora Wi-Fi integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
|
||||
from decora_wifi import DecoraWiFiSession
|
||||
from decora_wifi.models.iot_switch import IotSwitch
|
||||
from decora_wifi.models.person import Person
|
||||
from decora_wifi.models.residence import Residence
|
||||
from decora_wifi.models.residential_account import ResidentialAccount
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
PLATFORMS = [Platform.LIGHT]
|
||||
|
||||
type DecoraWifiConfigEntry = ConfigEntry[DecoraWifiData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DecoraWifiData:
|
||||
"""Runtime data for the Decora Wi-Fi integration."""
|
||||
|
||||
session: DecoraWiFiSession
|
||||
switches: list[IotSwitch]
|
||||
|
||||
|
||||
def _login_and_get_switches(email: str, password: str) -> DecoraWifiData:
|
||||
"""Log in and fetch all IoT switches. Runs in executor."""
|
||||
session = DecoraWiFiSession()
|
||||
success = session.login(email, password)
|
||||
|
||||
if success is None:
|
||||
raise ConfigEntryAuthFailed("Invalid credentials for myLeviton account")
|
||||
|
||||
perms = session.user.get_residential_permissions()
|
||||
all_switches: list[IotSwitch] = []
|
||||
for permission in perms:
|
||||
if permission.residentialAccountId is not None:
|
||||
acct = ResidentialAccount(session, permission.residentialAccountId)
|
||||
all_switches.extend(
|
||||
switch
|
||||
for residence in acct.get_residences()
|
||||
for switch in residence.get_iot_switches()
|
||||
)
|
||||
elif permission.residenceId is not None:
|
||||
residence = Residence(session, permission.residenceId)
|
||||
all_switches.extend(residence.get_iot_switches())
|
||||
|
||||
return DecoraWifiData(session, all_switches)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: DecoraWifiConfigEntry) -> bool:
|
||||
"""Set up Leviton Decora Wi-Fi from a config entry."""
|
||||
try:
|
||||
data = await hass.async_add_executor_job(
|
||||
_login_and_get_switches,
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
except ValueError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
"Failed to communicate with myLeviton service"
|
||||
) from err
|
||||
|
||||
entry.runtime_data = data
|
||||
|
||||
async def _logout(_: Event | None = None) -> None:
|
||||
with suppress(ValueError):
|
||||
await hass.async_add_executor_job(Person.logout, data.session)
|
||||
|
||||
entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _logout))
|
||||
entry.async_on_unload(_logout)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DecoraWifiConfigEntry) -> bool:
|
||||
"""Unload a Decora Wi-Fi config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user