forked from home-assistant/core
Compare commits
688 Commits
2024.7.0b9
...
compensati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02c0cfd680 | ||
|
|
22bb68d610 | ||
|
|
42fe1d6097 | ||
|
|
ebb450db48 | ||
|
|
d73e12df93 | ||
|
|
48978fb7f6 | ||
|
|
97aaae1372 | ||
|
|
cdf7ecebc0 | ||
|
|
cdb26ef8a3 | ||
|
|
76d6a1fe13 | ||
|
|
df8c949884 | ||
|
|
adb88f0165 | ||
|
|
c84fe04d6b | ||
|
|
86db976ed7 | ||
|
|
e269ff617c | ||
|
|
414aee395e | ||
|
|
3be95ebc87 | ||
|
|
ede130aa53 | ||
|
|
21d35ff311 | ||
|
|
8fb700c30e | ||
|
|
2148cfc899 | ||
|
|
c40a9ac4b5 | ||
|
|
ab7c4244d2 | ||
|
|
f40897b54c | ||
|
|
73475aa675 | ||
|
|
c223709c7c | ||
|
|
52454f5218 | ||
|
|
f94b28f72d | ||
|
|
acb4a92628 | ||
|
|
f8015a7abc | ||
|
|
4c972694cd | ||
|
|
c6f1ec34e2 | ||
|
|
43596f22a4 | ||
|
|
67fee5be31 | ||
|
|
c89de2e6a6 | ||
|
|
307ae53066 | ||
|
|
322553b8a9 | ||
|
|
fc770434f1 | ||
|
|
56b6747bc0 | ||
|
|
ea5eb0f8f2 | ||
|
|
bb81cfa57a | ||
|
|
22c89356c0 | ||
|
|
385576bfb2 | ||
|
|
90af40b5c4 | ||
|
|
417abda649 | ||
|
|
0462582da3 | ||
|
|
2fce71ea52 | ||
|
|
61111f5d71 | ||
|
|
70f05e5f13 | ||
|
|
311b1e236a | ||
|
|
e0c7073da1 | ||
|
|
088717926d | ||
|
|
664c4e6189 | ||
|
|
d87bbaa67a | ||
|
|
af6c28983d | ||
|
|
9fcfc6d9f3 | ||
|
|
e56f8d795f | ||
|
|
7ed5feee92 | ||
|
|
f3fe61f929 | ||
|
|
4fd5ced1f5 | ||
|
|
fce68018b7 | ||
|
|
abeac3f3aa | ||
|
|
924e767736 | ||
|
|
3142f52a79 | ||
|
|
a9c9963f0f | ||
|
|
71e5ffb2bd | ||
|
|
319fdf42c3 | ||
|
|
1e0c03257c | ||
|
|
5dbf907323 | ||
|
|
cd4358ed9e | ||
|
|
865190f0b0 | ||
|
|
0e0a339517 | ||
|
|
020961d2d8 | ||
|
|
c81d5a1ac2 | ||
|
|
8b729e3c7d | ||
|
|
02b12837d4 | ||
|
|
e812b0e02f | ||
|
|
b0837dd98f | ||
|
|
43806553fc | ||
|
|
f762359abf | ||
|
|
0213f1c5c0 | ||
|
|
42dcd693d5 | ||
|
|
1925614a14 | ||
|
|
42003ae5ac | ||
|
|
33df857283 | ||
|
|
6702d232e2 | ||
|
|
e9b7cc1eba | ||
|
|
65e03f952a | ||
|
|
30b5297fde | ||
|
|
86f7718c1f | ||
|
|
d10aa6656b | ||
|
|
ff59adc661 | ||
|
|
5d809f88ba | ||
|
|
8b0f28e7f4 | ||
|
|
7a337ecd1d | ||
|
|
fb758bd8b6 | ||
|
|
6f15352eda | ||
|
|
bf09cee66f | ||
|
|
0aa6a17da8 | ||
|
|
4245357403 | ||
|
|
72c3c0eb53 | ||
|
|
8d0d8fd006 | ||
|
|
9120115577 | ||
|
|
805d4fcc40 | ||
|
|
92e6dacf14 | ||
|
|
d783813ba0 | ||
|
|
5e56c27703 | ||
|
|
9d2cf70ba5 | ||
|
|
c5ced14473 | ||
|
|
bff663b3c1 | ||
|
|
f14777d0ae | ||
|
|
a4980cf0fd | ||
|
|
31dc80c616 | ||
|
|
898803abe9 | ||
|
|
5b25c24539 | ||
|
|
e44f00cf7c | ||
|
|
154da1b18b | ||
|
|
69ed730101 | ||
|
|
01861cd240 | ||
|
|
4498bf9ec4 | ||
|
|
4a22b95620 | ||
|
|
592cfc9c17 | ||
|
|
d4cc44d352 | ||
|
|
ba0cd595ff | ||
|
|
9ca398ef82 | ||
|
|
7aada39b77 | ||
|
|
a1fae0e0ce | ||
|
|
cfabb778dd | ||
|
|
48d9df74ed | ||
|
|
b07fcb6a4a | ||
|
|
3ea694a069 | ||
|
|
9321bba61e | ||
|
|
c13d5a663a | ||
|
|
6e85f09722 | ||
|
|
45843a3112 | ||
|
|
2a31774f91 | ||
|
|
f801649e9c | ||
|
|
60e1df2a87 | ||
|
|
5dbaaa9068 | ||
|
|
10f3813ffd | ||
|
|
71300b197a | ||
|
|
b754f03c11 | ||
|
|
e11d24f06f | ||
|
|
5f088e0501 | ||
|
|
93cf8c1311 | ||
|
|
3e8335f61b | ||
|
|
5f909d0c52 | ||
|
|
98651cbd2e | ||
|
|
19e272a901 | ||
|
|
10ee554f1f | ||
|
|
f7825eb5b1 | ||
|
|
8ab6a505a4 | ||
|
|
bd4ef4b2ff | ||
|
|
242b28377d | ||
|
|
b048ad8afe | ||
|
|
148803460b | ||
|
|
c4c623ea99 | ||
|
|
4d68ae1199 | ||
|
|
3059bf0536 | ||
|
|
1aa9588dcb | ||
|
|
00aafc0cf7 | ||
|
|
6350c5479b | ||
|
|
ad47a7b8c6 | ||
|
|
617db25efb | ||
|
|
ea65ff5876 | ||
|
|
cf4bd7fd1c | ||
|
|
e30f315565 | ||
|
|
cd478c356e | ||
|
|
27a2114ba9 | ||
|
|
4cf733b9a2 | ||
|
|
8a28cbe9e2 | ||
|
|
6129a37bb0 | ||
|
|
0558e480ce | ||
|
|
f126360c67 | ||
|
|
833ac4db49 | ||
|
|
72458d143d | ||
|
|
12228d8a00 | ||
|
|
8110b60fc9 | ||
|
|
cd72bdd851 | ||
|
|
26bc37195e | ||
|
|
2def33b168 | ||
|
|
a5c9aa44bd | ||
|
|
751935539a | ||
|
|
866cdcc993 | ||
|
|
eb0f6f7088 | ||
|
|
fb8eeac563 | ||
|
|
1fefd396b9 | ||
|
|
67facdf3a5 | ||
|
|
27975c7517 | ||
|
|
792c6a9cd9 | ||
|
|
b6609fa77c | ||
|
|
a66631f1ee | ||
|
|
de8415d5c7 | ||
|
|
0d48d5a5ec | ||
|
|
76bdc4f5c4 | ||
|
|
6937aed9fe | ||
|
|
959298d121 | ||
|
|
9b4e64ab62 | ||
|
|
275a7499b1 | ||
|
|
e2141dc208 | ||
|
|
790d22dc46 | ||
|
|
37ffe09add | ||
|
|
dc5ae9e0b2 | ||
|
|
4c1cb6cce8 | ||
|
|
643dcb1429 | ||
|
|
e9b62dd203 | ||
|
|
fc6906a12f | ||
|
|
846528131e | ||
|
|
1c2e6ab3db | ||
|
|
8e45eb6168 | ||
|
|
d95b28bdee | ||
|
|
17172d841c | ||
|
|
5ea1601c2c | ||
|
|
44f989ae38 | ||
|
|
f1693f2fde | ||
|
|
52de706a85 | ||
|
|
6e8cfb3376 | ||
|
|
ac8ea8356c | ||
|
|
f115efe51d | ||
|
|
99476d9337 | ||
|
|
98370fd9a0 | ||
|
|
d113ea8e47 | ||
|
|
ec536bda3d | ||
|
|
490dd53edf | ||
|
|
131d9ec51b | ||
|
|
48145c1a7d | ||
|
|
cf34b46b5a | ||
|
|
3c14aa12ab | ||
|
|
df9ced9768 | ||
|
|
b5a0e89c03 | ||
|
|
f46c127423 | ||
|
|
e04d612874 | ||
|
|
2bc7904b51 | ||
|
|
be0cf545b2 | ||
|
|
8f7c3da456 | ||
|
|
17daccd38a | ||
|
|
8ff4991f07 | ||
|
|
43481ffeac | ||
|
|
ac8bbe9db4 | ||
|
|
696ae91c35 | ||
|
|
f29094f41d | ||
|
|
294c31f6f5 | ||
|
|
cce77affc7 | ||
|
|
1ec95f900d | ||
|
|
a358609609 | ||
|
|
18370bdbb4 | ||
|
|
e86f2220d0 | ||
|
|
9f839dbb77 | ||
|
|
1b8944dab6 | ||
|
|
9d204613e8 | ||
|
|
f2c9188e46 | ||
|
|
1561324274 | ||
|
|
34b5df50cc | ||
|
|
130dbcb3f1 | ||
|
|
89ffee9ad5 | ||
|
|
94db251aea | ||
|
|
ad2d794fd6 | ||
|
|
178655647d | ||
|
|
6e5c3904b6 | ||
|
|
a773d0c8f6 | ||
|
|
6eeb70151d | ||
|
|
fd815beb2d | ||
|
|
8c459959ee | ||
|
|
4b4b17e774 | ||
|
|
9383920b1a | ||
|
|
ffc39585ed | ||
|
|
45ab9cae1a | ||
|
|
bf8cef3337 | ||
|
|
324755b8f5 | ||
|
|
94d010a4c0 | ||
|
|
4289efb506 | ||
|
|
b9cfd4e8ce | ||
|
|
7332bc5faf | ||
|
|
641d6382fd | ||
|
|
288c297c9c | ||
|
|
e4731c07cb | ||
|
|
d42dced852 | ||
|
|
d0c10c961d | ||
|
|
0088765268 | ||
|
|
afb184db73 | ||
|
|
eb5a98e7ea | ||
|
|
2ab02c06c6 | ||
|
|
0cf5b4f965 | ||
|
|
ad02afe7be | ||
|
|
daaf35d4c1 | ||
|
|
229e54d0b1 | ||
|
|
97de9c9f69 | ||
|
|
700675042b | ||
|
|
1c1e1a7bfa | ||
|
|
98dfb47448 | ||
|
|
1b42b32ac1 | ||
|
|
dbe98de82a | ||
|
|
b28f528a7a | ||
|
|
d3f424227f | ||
|
|
22718ca32a | ||
|
|
2b9bddc3fc | ||
|
|
213bbae63c | ||
|
|
def27a082c | ||
|
|
c3830a58cc | ||
|
|
b054c037fe | ||
|
|
dcef25c0fa | ||
|
|
5a24ee0bc0 | ||
|
|
e71f6c5948 | ||
|
|
cdb2ec4231 | ||
|
|
adee8094e7 | ||
|
|
6c42596bdd | ||
|
|
b3a62a97b4 | ||
|
|
62d9020261 | ||
|
|
e47cbf3cf7 | ||
|
|
d799a4575b | ||
|
|
b406438fa7 | ||
|
|
b14f22926a | ||
|
|
276f6c7ee7 | ||
|
|
83fac6192d | ||
|
|
84a8259103 | ||
|
|
3ca66be268 | ||
|
|
001bb3a04e | ||
|
|
50cc31e9cc | ||
|
|
0afebf744f | ||
|
|
df7be501d3 | ||
|
|
ebe7a4747d | ||
|
|
10d3c3d341 | ||
|
|
6ab6ce30ef | ||
|
|
fe0bafd067 | ||
|
|
04a6285e62 | ||
|
|
e07bf61f03 | ||
|
|
df1b02d44e | ||
|
|
79f4cc9c12 | ||
|
|
7d5341cab2 | ||
|
|
d5135d4956 | ||
|
|
950c72a04c | ||
|
|
6df15ad8fc | ||
|
|
a1e6f8c2ec | ||
|
|
092e362f01 | ||
|
|
28f06cb5a0 | ||
|
|
411630429d | ||
|
|
9a1f7f020c | ||
|
|
d12f2384c5 | ||
|
|
24c82c2475 | ||
|
|
b949240d4a | ||
|
|
f78933235d | ||
|
|
ece8b74967 | ||
|
|
1d8382a498 | ||
|
|
873d96bab3 | ||
|
|
31ed32da6c | ||
|
|
f1d6ad9073 | ||
|
|
d1264655a0 | ||
|
|
ad1ba1a5e5 | ||
|
|
255778d0c7 | ||
|
|
3c69301365 | ||
|
|
67a4c2c884 | ||
|
|
d429bcef16 | ||
|
|
02e7707f91 | ||
|
|
ee2df9c4b3 | ||
|
|
0be1f773a2 | ||
|
|
a6f6221f16 | ||
|
|
4958e8e5c1 | ||
|
|
8aedb1201d | ||
|
|
aa74ad0061 | ||
|
|
43e4223a8e | ||
|
|
869f24df49 | ||
|
|
cf96084ea3 | ||
|
|
1eec49696a | ||
|
|
1f22f0d89b | ||
|
|
dd8ba0828a | ||
|
|
c9acd1711c | ||
|
|
4589be2d11 | ||
|
|
d55d02623a | ||
|
|
24f6e6e885 | ||
|
|
ac9c08f52c | ||
|
|
8a5b201d75 | ||
|
|
0e9acf2685 | ||
|
|
3b023367d7 | ||
|
|
3dbab1a580 | ||
|
|
1144e23d8d | ||
|
|
e8ef2c2822 | ||
|
|
a4d4fc6827 | ||
|
|
7002042119 | ||
|
|
c59fc4e3c7 | ||
|
|
cc2782edc7 | ||
|
|
b6a23fad35 | ||
|
|
cfef09d653 | ||
|
|
84d8bc711d | ||
|
|
595e688c56 | ||
|
|
408e524551 | ||
|
|
73716ea529 | ||
|
|
c9240b8e34 | ||
|
|
7958c0825e | ||
|
|
291f309c0e | ||
|
|
f85c356522 | ||
|
|
8017bc6776 | ||
|
|
2040c285b1 | ||
|
|
53767b6159 | ||
|
|
4881205831 | ||
|
|
5029da6919 | ||
|
|
61f1c8d963 | ||
|
|
48172b0426 | ||
|
|
2b64f6f2ab | ||
|
|
1080a4ef1e | ||
|
|
44c89e6c3b | ||
|
|
d94b36cfbb | ||
|
|
85168239cd | ||
|
|
547b24ce58 | ||
|
|
e8bcb3e11e | ||
|
|
c89a9b5ce0 | ||
|
|
13631250b4 | ||
|
|
6621cf475a | ||
|
|
36e74cd9a6 | ||
|
|
16827ea09e | ||
|
|
c4956b66b0 | ||
|
|
e4a0a21b67 | ||
|
|
bc363c385f | ||
|
|
7a9792c111 | ||
|
|
1a715d7b89 | ||
|
|
46a488d871 | ||
|
|
0b76d5c9ca | ||
|
|
355c7399d7 | ||
|
|
e26b4554e6 | ||
|
|
976cb434c9 | ||
|
|
f284aa41eb | ||
|
|
1332e39f9e | ||
|
|
8709c668cc | ||
|
|
87f7703f3c | ||
|
|
e7ffd7b9ad | ||
|
|
9b2233e65e | ||
|
|
fbb98a668c | ||
|
|
a885bdfe76 | ||
|
|
ac57eb7614 | ||
|
|
84204c38be | ||
|
|
febd1a3772 | ||
|
|
1665cb40ac | ||
|
|
c33cbf8312 | ||
|
|
23e061ccbd | ||
|
|
399548a973 | ||
|
|
a4d889e958 | ||
|
|
510315732a | ||
|
|
476efb1d36 | ||
|
|
bdc6805771 | ||
|
|
7d31d553d0 | ||
|
|
9749cf113a | ||
|
|
0e52d149e4 | ||
|
|
0d0ca22103 | ||
|
|
52627b9aed | ||
|
|
ba7e45e157 | ||
|
|
592ef59c5a | ||
|
|
195f07a18a | ||
|
|
5b39989869 | ||
|
|
b8b7c23258 | ||
|
|
faf43ed4c7 | ||
|
|
bd234db48f | ||
|
|
2edb7eb42c | ||
|
|
72d706ab52 | ||
|
|
e322cada48 | ||
|
|
baf2ebf1f2 | ||
|
|
326d24d78b | ||
|
|
3adea1ada9 | ||
|
|
02dffcde1a | ||
|
|
cdc3897319 | ||
|
|
22f5f59478 | ||
|
|
76a62028ad | ||
|
|
6fd1f0a34f | ||
|
|
e544550380 | ||
|
|
8819a9aa52 | ||
|
|
1f6744847d | ||
|
|
4a8436d6bc | ||
|
|
b13e78f3a3 | ||
|
|
71b7ee40e5 | ||
|
|
9ca9377cad | ||
|
|
e3516be3e3 | ||
|
|
fac511aa46 | ||
|
|
3df3e6d081 | ||
|
|
07d80d5ad9 | ||
|
|
2d054fb5df | ||
|
|
2635573bbc | ||
|
|
90d622cd02 | ||
|
|
0ffebd4853 | ||
|
|
383de96549 | ||
|
|
690164a518 | ||
|
|
167a8c6613 | ||
|
|
07f095aa42 | ||
|
|
c2dc9e9b67 | ||
|
|
b5367e5994 | ||
|
|
1209abc944 | ||
|
|
a29dc4ef1e | ||
|
|
7a3039aecb | ||
|
|
d506c30b38 | ||
|
|
a0b604f98c | ||
|
|
001ee0cc0b | ||
|
|
e5c7ff6a5b | ||
|
|
918ac5d67c | ||
|
|
afb0a6e0ab | ||
|
|
b3a50893cf | ||
|
|
c6cfe073ea | ||
|
|
d53cfbbb4e | ||
|
|
8354aa434e | ||
|
|
5ce54c2174 | ||
|
|
813fee663e | ||
|
|
dcf4e91234 | ||
|
|
361e81821c | ||
|
|
52b743e88a | ||
|
|
788d1999ff | ||
|
|
c4903dd982 | ||
|
|
8a23e37837 | ||
|
|
77fc1c991c | ||
|
|
ce54ca9c8e | ||
|
|
c8bb64882e | ||
|
|
2815c43f3e | ||
|
|
38aa6bcf19 | ||
|
|
2506acc095 | ||
|
|
c9911fa8ce | ||
|
|
bc3562a9e8 | ||
|
|
5513682de4 | ||
|
|
61b2e4ca32 | ||
|
|
546d6b22f1 | ||
|
|
414525503c | ||
|
|
4cc414fbf8 | ||
|
|
09ff44e59e | ||
|
|
44640ef9e8 | ||
|
|
f08638eead | ||
|
|
921430d497 | ||
|
|
2f0dd6f704 | ||
|
|
f11b316dac | ||
|
|
ca55986057 | ||
|
|
aa5ebaf613 | ||
|
|
88583149ea | ||
|
|
d5d77db4f9 | ||
|
|
295cfd26aa | ||
|
|
74c2f000d8 | ||
|
|
269b8b07c4 | ||
|
|
6af9527310 | ||
|
|
eceecbb07b | ||
|
|
1a63bb89cb | ||
|
|
2f5ec41fa6 | ||
|
|
f672eec515 | ||
|
|
419d89f863 | ||
|
|
05ffd637f5 | ||
|
|
b375f5227b | ||
|
|
8b3319b772 | ||
|
|
289a630578 | ||
|
|
bb62a8a7dc | ||
|
|
e961ddd5fe | ||
|
|
be68255ca4 | ||
|
|
57aced50aa | ||
|
|
5deb69d492 | ||
|
|
255cc9ed74 | ||
|
|
7aca7cf858 | ||
|
|
ca7fb906cc | ||
|
|
d15d001cfc | ||
|
|
d55be79e6a | ||
|
|
bf608691d5 | ||
|
|
75e3afd369 | ||
|
|
72fdebeb88 | ||
|
|
f65304957a | ||
|
|
5280291f98 | ||
|
|
7172d798f8 | ||
|
|
e0b0959b3f | ||
|
|
bcee5f4d9f | ||
|
|
559caf4179 | ||
|
|
b5f1076bb2 | ||
|
|
8f98fb2ec4 | ||
|
|
25932dff28 | ||
|
|
0ab7647fea | ||
|
|
ce34a5e495 | ||
|
|
7d8cbbaacb | ||
|
|
f07f9062c1 | ||
|
|
5a6deddd30 | ||
|
|
c5804d362c | ||
|
|
852bb19223 | ||
|
|
6ed0960648 | ||
|
|
8f6addcc61 | ||
|
|
2cfd6d53bd | ||
|
|
0dfb5bd7d9 | ||
|
|
d4ecbc91c3 | ||
|
|
04ab74589a | ||
|
|
5995459de5 | ||
|
|
ba38f2e43b | ||
|
|
cef7def024 | ||
|
|
8ed11d4b90 | ||
|
|
b03c10647e | ||
|
|
a6454cf3c7 | ||
|
|
3549aaf69c | ||
|
|
a3394675f3 | ||
|
|
97ef56d264 | ||
|
|
4fb0621027 | ||
|
|
c029c534d6 | ||
|
|
f4224a0327 | ||
|
|
3e8773c0d5 | ||
|
|
0ea1677f51 | ||
|
|
e907c45981 | ||
|
|
a8f4684929 | ||
|
|
d03a6f84a3 | ||
|
|
9505fcdd7d | ||
|
|
3e3ab7a134 | ||
|
|
b56c4a757c | ||
|
|
984bbf8851 | ||
|
|
1fdd056c0e | ||
|
|
c7906f90a3 | ||
|
|
d427dff68d | ||
|
|
f69b850b1a | ||
|
|
d0ce0d562e | ||
|
|
2e031d0194 | ||
|
|
d2a457c24f | ||
|
|
c385deb6a3 | ||
|
|
6ef8e87f88 | ||
|
|
4437c4a204 | ||
|
|
23e5e25149 | ||
|
|
c13786c952 | ||
|
|
6f8c9c28e7 | ||
|
|
0fdf037ba0 | ||
|
|
3d580259e1 | ||
|
|
6d93695e2c | ||
|
|
540da3cac6 | ||
|
|
6286177041 | ||
|
|
9b980602c9 | ||
|
|
1e72c2f94d | ||
|
|
5e39faa9f8 | ||
|
|
84de2da19f | ||
|
|
03c6e0c55f | ||
|
|
c98e70a6dc | ||
|
|
827bfa89b3 | ||
|
|
e0e744aed7 | ||
|
|
ec069f9084 | ||
|
|
4e34d02d2d | ||
|
|
e764afecac | ||
|
|
f3761a8e53 | ||
|
|
bccd5c8c35 | ||
|
|
440928bcbe | ||
|
|
338687522a | ||
|
|
6421612429 | ||
|
|
c4ab0dcd74 | ||
|
|
195643d916 | ||
|
|
aaef31958b | ||
|
|
629dab238f | ||
|
|
c419d226d5 | ||
|
|
f4b76406f2 | ||
|
|
eb2d2ce1b3 | ||
|
|
89dfca962f | ||
|
|
d423dae8ac | ||
|
|
f3c76cd698 | ||
|
|
0c910bc000 | ||
|
|
b9e01b9253 | ||
|
|
e446875c7e | ||
|
|
3895252965 | ||
|
|
9758b08036 | ||
|
|
970dd99226 | ||
|
|
0d53ce4fb8 | ||
|
|
8de771de96 | ||
|
|
1f54180807 | ||
|
|
9f6783dcf5 | ||
|
|
cb92510571 | ||
|
|
a93855ded3 | ||
|
|
a165064e9d | ||
|
|
54a5a3e3fb | ||
|
|
9aa2cc11e9 | ||
|
|
4f7c6bdce8 | ||
|
|
022f545342 | ||
|
|
06f495dd45 | ||
|
|
85629dc31e | ||
|
|
5634741ce2 | ||
|
|
3e9b57cc07 | ||
|
|
c9c573dbce | ||
|
|
5503379a3b | ||
|
|
617ab48fa9 | ||
|
|
28e72753ad | ||
|
|
a5a631148e | ||
|
|
f189d87fe9 | ||
|
|
5e1c8b0c54 | ||
|
|
2449e2029e | ||
|
|
32e64f7911 | ||
|
|
b7a65d9a82 | ||
|
|
9d7078e1fa | ||
|
|
1973c604b6 | ||
|
|
6bceb8ec48 | ||
|
|
fcfb580f0c | ||
|
|
516b9126b7 | ||
|
|
2146a4729b | ||
|
|
9b2915efed | ||
|
|
dd6cc82f70 | ||
|
|
8839a71adc | ||
|
|
f1a57d69cc | ||
|
|
702d53ca30 | ||
|
|
4c6cbadc11 | ||
|
|
d8ab2debfd | ||
|
|
31e9de3b95 | ||
|
|
ed1eb8ac9c | ||
|
|
d09a274548 | ||
|
|
02b142fbde | ||
|
|
33b4f40b2a |
@@ -49,6 +49,7 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/tts/**
|
||||
- homeassistant/components/update/**
|
||||
- homeassistant/components/vacuum/**
|
||||
- homeassistant/components/valve/**
|
||||
- homeassistant/components/water_heater/**
|
||||
- homeassistant/components/weather/**
|
||||
|
||||
|
||||
1733
.coveragerc
1733
.coveragerc
File diff suppressed because it is too large
Load Diff
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -74,7 +74,6 @@ If the code communicates with devices, web services, or third-party tools:
|
||||
- [ ] New or updated dependencies have been added to `requirements_all.txt`.
|
||||
Updated by running `python3 -m script.gen_requirements_all`.
|
||||
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
|
||||
- [ ] Untested files have been added to `.coveragerc`.
|
||||
|
||||
<!--
|
||||
This project is very active and we have a high turnover of pull requests.
|
||||
|
||||
12
.github/workflows/builder.yml
vendored
12
.github/workflows/builder.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -453,12 +453,12 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
name: translations
|
||||
|
||||
|
||||
102
.github/workflows/ci.yaml
vendored
102
.github/workflows/ci.yaml
vendored
@@ -36,7 +36,7 @@ env:
|
||||
CACHE_VERSION: 9
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 8
|
||||
HA_SHORT_VERSION: "2024.7"
|
||||
HA_SHORT_VERSION: "2024.8"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -274,7 +274,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -314,7 +314,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -353,7 +353,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -448,7 +448,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -522,11 +522,17 @@ jobs:
|
||||
- info
|
||||
- base
|
||||
steps:
|
||||
- name: Install additional OS dependencies
|
||||
run: |
|
||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install \
|
||||
libturbojpeg
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -558,7 +564,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -576,6 +582,46 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.gen_requirements_all validate
|
||||
|
||||
audit-licenses:
|
||||
name: Audit licenses
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
if: |
|
||||
needs.info.outputs.requirements == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.0.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Run pip-licenses
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pip-licenses --format=json --output-file=licenses.json
|
||||
- name: Upload licenses
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: licenses
|
||||
path: licenses.json
|
||||
- name: Process licenses
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python -m script.licenses
|
||||
|
||||
pylint:
|
||||
name: Check pylint
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -591,7 +637,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -636,7 +682,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -680,7 +726,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -754,7 +800,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -772,7 +818,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
||||
- name: Upload pytest_buckets
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: pytest_buckets
|
||||
path: pytest_buckets.txt
|
||||
@@ -817,7 +863,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -836,7 +882,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
name: pytest_buckets
|
||||
- name: Compile English translations
|
||||
@@ -871,14 +917,14 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: pytest-*.txt
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
@@ -935,7 +981,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -996,7 +1042,7 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -1004,7 +1050,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -1060,7 +1106,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1122,7 +1168,7 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1130,7 +1176,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1154,7 +1200,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -1205,7 +1251,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1263,14 +1309,14 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: pytest-*.txt
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
@@ -1291,7 +1337,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.25.10
|
||||
uses: github/codeql-action/init@v3.25.11
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.25.10
|
||||
uses: github/codeql-action/analyze@v3.25.11
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
||||
28
.github/workflows/wheels.yml
vendored
28
.github/workflows/wheels.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -82,14 +82,14 @@ jobs:
|
||||
) > .env_file
|
||||
|
||||
- name: Upload env_file
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
overwrite: true
|
||||
|
||||
- name: Upload requirements_diff
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: requirements_diff
|
||||
path: ./requirements_diff.txt
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
python -m script.gen_requirements_all ci
|
||||
|
||||
- name: Upload requirements_all_wheels
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
path: ./requirements_all_wheels_*.txt
|
||||
@@ -121,17 +121,17 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2024.01.0
|
||||
uses: home-assistant/wheels@2024.07.1
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@@ -159,17 +159,17 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
sed -i "/numpy/d" homeassistant/package_constraints.txt
|
||||
|
||||
- name: Build wheels (old cython)
|
||||
uses: home-assistant/wheels@2024.01.0
|
||||
uses: home-assistant/wheels@2024.07.1
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@@ -218,7 +218,7 @@ jobs:
|
||||
pip: "'cython<3'"
|
||||
|
||||
- name: Build wheels (part 1)
|
||||
uses: home-assistant/wheels@2024.01.0
|
||||
uses: home-assistant/wheels@2024.07.1
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@@ -232,7 +232,7 @@ jobs:
|
||||
requirements: "requirements_all.txtaa"
|
||||
|
||||
- name: Build wheels (part 2)
|
||||
uses: home-assistant/wheels@2024.01.0
|
||||
uses: home-assistant/wheels@2024.07.1
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@@ -246,7 +246,7 @@ jobs:
|
||||
requirements: "requirements_all.txtab"
|
||||
|
||||
- name: Build wheels (part 3)
|
||||
uses: home-assistant/wheels@2024.01.0
|
||||
uses: home-assistant/wheels@2024.07.1
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.4.9
|
||||
rev: v0.5.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
@@ -83,7 +83,7 @@ repos:
|
||||
pass_filenames: false
|
||||
language: script
|
||||
types: [text]
|
||||
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$
|
||||
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$
|
||||
- id: hassfest-metadata
|
||||
name: hassfest-metadata
|
||||
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
|
||||
|
||||
@@ -97,6 +97,7 @@ homeassistant.components.assist_pipeline.*
|
||||
homeassistant.components.asterisk_cdr.*
|
||||
homeassistant.components.asterisk_mbox.*
|
||||
homeassistant.components.asuswrt.*
|
||||
homeassistant.components.autarco.*
|
||||
homeassistant.components.auth.*
|
||||
homeassistant.components.automation.*
|
||||
homeassistant.components.awair.*
|
||||
@@ -287,6 +288,7 @@ homeassistant.components.logger.*
|
||||
homeassistant.components.london_underground.*
|
||||
homeassistant.components.lookin.*
|
||||
homeassistant.components.luftdaten.*
|
||||
homeassistant.components.madvr.*
|
||||
homeassistant.components.mailbox.*
|
||||
homeassistant.components.map.*
|
||||
homeassistant.components.mastodon.*
|
||||
|
||||
42
.vscode/launch.json
vendored
42
.vscode/launch.json
vendored
@@ -6,38 +6,52 @@
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Home Assistant",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "homeassistant",
|
||||
"justMyCode": false,
|
||||
"args": ["--debug", "-c", "config"],
|
||||
"args": [
|
||||
"--debug",
|
||||
"-c",
|
||||
"config"
|
||||
],
|
||||
"preLaunchTask": "Compile English translations"
|
||||
},
|
||||
{
|
||||
"name": "Home Assistant (skip pip)",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "homeassistant",
|
||||
"justMyCode": false,
|
||||
"args": ["--debug", "-c", "config", "--skip-pip"],
|
||||
"args": [
|
||||
"--debug",
|
||||
"-c",
|
||||
"config",
|
||||
"--skip-pip"
|
||||
],
|
||||
"preLaunchTask": "Compile English translations"
|
||||
},
|
||||
{
|
||||
"name": "Home Assistant: Changed tests",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "pytest",
|
||||
"justMyCode": false,
|
||||
"args": ["--timeout=10", "--picked"],
|
||||
"args": [
|
||||
"--timeout=10",
|
||||
"--picked"
|
||||
],
|
||||
},
|
||||
{
|
||||
// Debug by attaching to local Home Assistant server using Remote Python Debugger.
|
||||
// See https://www.home-assistant.io/integrations/debugpy/
|
||||
"name": "Home Assistant: Attach Local",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"port": 5678,
|
||||
"host": "localhost",
|
||||
"connect": {
|
||||
"port": 5678,
|
||||
"host": "localhost"
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
@@ -49,10 +63,12 @@
|
||||
// Debug by attaching to remote Home Assistant server using Remote Python Debugger.
|
||||
// See https://www.home-assistant.io/integrations/debugpy/
|
||||
"name": "Home Assistant: Attach Remote",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"port": 5678,
|
||||
"host": "homeassistant.local",
|
||||
"connect": {
|
||||
"port": 5678,
|
||||
"host": "homeassistant.local"
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
@@ -61,4 +77,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
1
.vscode/tasks.json
vendored
1
.vscode/tasks.json
vendored
@@ -76,6 +76,7 @@
|
||||
"detail": "Generate code coverage report for a given integration.",
|
||||
"type": "shell",
|
||||
"command": "python3 -m pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0 --numprocesses=auto",
|
||||
"dependsOn": ["Compile English translations"],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
|
||||
30
CODEOWNERS
30
CODEOWNERS
@@ -155,6 +155,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/aurora_abb_powerone/ @davet2001
|
||||
/homeassistant/components/aussie_broadband/ @nickw444 @Bre77
|
||||
/tests/components/aussie_broadband/ @nickw444 @Bre77
|
||||
/homeassistant/components/autarco/ @klaasnicolaas
|
||||
/tests/components/autarco/ @klaasnicolaas
|
||||
/homeassistant/components/auth/ @home-assistant/core
|
||||
/tests/components/auth/ @home-assistant/core
|
||||
/homeassistant/components/automation/ @home-assistant/core
|
||||
@@ -237,6 +239,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ccm15/ @ocalvo
|
||||
/homeassistant/components/cert_expiry/ @jjlawren
|
||||
/tests/components/cert_expiry/ @jjlawren
|
||||
/homeassistant/components/chacon_dio/ @cnico
|
||||
/tests/components/chacon_dio/ @cnico
|
||||
/homeassistant/components/cisco_ios/ @fbradyirl
|
||||
/homeassistant/components/cisco_mobility_express/ @fbradyirl
|
||||
/homeassistant/components/cisco_webex_teams/ @fbradyirl
|
||||
@@ -358,8 +362,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ecoforest/ @pjanuario
|
||||
/homeassistant/components/econet/ @w1ll1am23
|
||||
/tests/components/econet/ @w1ll1am23
|
||||
/homeassistant/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @Augar
|
||||
/tests/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @Augar
|
||||
/homeassistant/components/ecovacs/ @mib1185 @edenhaus @Augar
|
||||
/tests/components/ecovacs/ @mib1185 @edenhaus @Augar
|
||||
/homeassistant/components/ecowitt/ @pvizeli
|
||||
/tests/components/ecowitt/ @pvizeli
|
||||
/homeassistant/components/efergy/ @tkdrob
|
||||
@@ -396,8 +400,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/enigma2/ @autinerd
|
||||
/homeassistant/components/enocean/ @bdurrer
|
||||
/tests/components/enocean/ @bdurrer
|
||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
|
||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
|
||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
|
||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
|
||||
/homeassistant/components/entur_public_transport/ @hfurubotten
|
||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||
@@ -429,6 +433,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/fan/ @home-assistant/core
|
||||
/homeassistant/components/fastdotcom/ @rohankapoorcom @erwindouna
|
||||
/tests/components/fastdotcom/ @rohankapoorcom @erwindouna
|
||||
/homeassistant/components/feedreader/ @mib1185
|
||||
/tests/components/feedreader/ @mib1185
|
||||
/homeassistant/components/fibaro/ @rappenze
|
||||
/tests/components/fibaro/ @rappenze
|
||||
/homeassistant/components/file/ @fabaff
|
||||
@@ -701,6 +707,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/isal/ @bdraco
|
||||
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||
/homeassistant/components/israel_rail/ @shaiu
|
||||
/tests/components/israel_rail/ @shaiu
|
||||
/homeassistant/components/iss/ @DurgNomis-drol
|
||||
/tests/components/iss/ @DurgNomis-drol
|
||||
/homeassistant/components/ista_ecotrend/ @tr4nt0r
|
||||
@@ -777,6 +785,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||
/homeassistant/components/lidarr/ @tkdrob
|
||||
/tests/components/lidarr/ @tkdrob
|
||||
/homeassistant/components/lifx/ @Djelibeybi
|
||||
/tests/components/lifx/ @Djelibeybi
|
||||
/homeassistant/components/light/ @home-assistant/core
|
||||
/tests/components/light/ @home-assistant/core
|
||||
/homeassistant/components/linear_garage_door/ @IceBotYT
|
||||
@@ -821,13 +831,15 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
|
||||
/homeassistant/components/lyric/ @timmo001
|
||||
/tests/components/lyric/ @timmo001
|
||||
/homeassistant/components/madvr/ @iloveicedgreentea
|
||||
/tests/components/madvr/ @iloveicedgreentea
|
||||
/homeassistant/components/mastodon/ @fabaff
|
||||
/homeassistant/components/matrix/ @PaarthShah
|
||||
/tests/components/matrix/ @PaarthShah
|
||||
/homeassistant/components/matter/ @home-assistant/matter
|
||||
/tests/components/matter/ @home-assistant/matter
|
||||
/homeassistant/components/mealie/ @joostlek
|
||||
/tests/components/mealie/ @joostlek
|
||||
/homeassistant/components/mealie/ @joostlek @andrew-codechimp
|
||||
/tests/components/mealie/ @joostlek @andrew-codechimp
|
||||
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
||||
/tests/components/meater/ @Sotolotl @emontnemery
|
||||
/homeassistant/components/medcom_ble/ @elafargue
|
||||
@@ -985,8 +997,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/ollama/ @synesthesiam
|
||||
/tests/components/ollama/ @synesthesiam
|
||||
/homeassistant/components/ombi/ @larssont
|
||||
/homeassistant/components/omnilogic/ @oliver84 @djtimca @gentoosu
|
||||
/tests/components/omnilogic/ @oliver84 @djtimca @gentoosu
|
||||
/homeassistant/components/onboarding/ @home-assistant/core
|
||||
/tests/components/onboarding/ @home-assistant/core
|
||||
/homeassistant/components/oncue/ @bdraco @peterager
|
||||
@@ -1202,6 +1212,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/rtsp_to_webrtc/ @allenporter
|
||||
/homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
||||
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
||||
/homeassistant/components/russound_rio/ @noahhusby
|
||||
/tests/components/russound_rio/ @noahhusby
|
||||
/homeassistant/components/ruuvi_gateway/ @akx
|
||||
/tests/components/ruuvi_gateway/ @akx
|
||||
/homeassistant/components/ruuvitag_ble/ @akx
|
||||
@@ -1269,6 +1281,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/sighthound/ @robmarkcole
|
||||
/homeassistant/components/signal_messenger/ @bbernhard
|
||||
/tests/components/signal_messenger/ @bbernhard
|
||||
/homeassistant/components/simplefin/ @scottg489 @jeeftor
|
||||
/tests/components/simplefin/ @scottg489 @jeeftor
|
||||
/homeassistant/components/simplepush/ @engrbm87
|
||||
/tests/components/simplepush/ @engrbm87
|
||||
/homeassistant/components/simplisafe/ @bachya
|
||||
|
||||
@@ -4,7 +4,7 @@ coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 90
|
||||
target: auto
|
||||
threshold: 0.09
|
||||
required:
|
||||
target: auto
|
||||
|
||||
@@ -363,15 +363,15 @@ class AuthManager:
|
||||
local_only: bool | None = None,
|
||||
) -> None:
|
||||
"""Update a user."""
|
||||
kwargs: dict[str, Any] = {}
|
||||
|
||||
for attr_name, value in (
|
||||
("name", name),
|
||||
("group_ids", group_ids),
|
||||
("local_only", local_only),
|
||||
):
|
||||
if value is not None:
|
||||
kwargs[attr_name] = value
|
||||
kwargs: dict[str, Any] = {
|
||||
attr_name: value
|
||||
for attr_name, value in (
|
||||
("name", name),
|
||||
("group_ids", group_ids),
|
||||
("local_only", local_only),
|
||||
)
|
||||
if value is not None
|
||||
}
|
||||
await self._store.async_update_user(user, **kwargs)
|
||||
|
||||
if is_active is not None:
|
||||
|
||||
@@ -105,14 +105,18 @@ class AuthStore:
|
||||
"perm_lookup": self._perm_lookup,
|
||||
}
|
||||
|
||||
for attr_name, value in (
|
||||
("is_owner", is_owner),
|
||||
("is_active", is_active),
|
||||
("local_only", local_only),
|
||||
("system_generated", system_generated),
|
||||
):
|
||||
if value is not None:
|
||||
kwargs[attr_name] = value
|
||||
kwargs.update(
|
||||
{
|
||||
attr_name: value
|
||||
for attr_name, value in (
|
||||
("is_owner", is_owner),
|
||||
("is_active", is_active),
|
||||
("local_only", local_only),
|
||||
("system_generated", system_generated),
|
||||
)
|
||||
if value is not None
|
||||
}
|
||||
)
|
||||
|
||||
new_user = models.User(**kwargs)
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ from .helpers import (
|
||||
)
|
||||
from .helpers.dispatcher import async_dispatcher_send_internal
|
||||
from .helpers.storage import get_internal_store_manager
|
||||
from .helpers.system_info import async_get_system_info
|
||||
from .helpers.system_info import async_get_system_info, is_official_image
|
||||
from .helpers.typing import ConfigType
|
||||
from .setup import (
|
||||
# _setup_started is marked as protected to make it clear
|
||||
@@ -104,7 +104,7 @@ from .setup import (
|
||||
from .util.async_ import create_eager_task
|
||||
from .util.hass_dict import HassKey
|
||||
from .util.logging import async_activate_log_queue_handler
|
||||
from .util.package import async_get_user_site, is_virtual_env
|
||||
from .util.package import async_get_user_site, is_docker_env, is_virtual_env
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
# Ensure anyio backend is imported to avoid it being imported in the event loop
|
||||
@@ -407,6 +407,10 @@ def _init_blocking_io_modules_in_executor() -> None:
|
||||
# Initialize the mimetypes module to avoid blocking calls
|
||||
# to the filesystem to load the mime.types file.
|
||||
mimetypes.init()
|
||||
# Initialize is_official_image and is_docker_env to avoid blocking calls
|
||||
# to the filesystem.
|
||||
is_official_image()
|
||||
is_docker_env()
|
||||
|
||||
|
||||
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
},
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["jaraco.abode", "lomond"],
|
||||
"requirements": ["jaraco.abode==5.1.2"]
|
||||
"requirements": ["jaraco.abode==5.2.1"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.5.3"]
|
||||
"requirements": ["aioairzone-cloud==0.5.4"]
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Generator
|
||||
|
||||
from homeassistant.components import (
|
||||
button,
|
||||
climate,
|
||||
@@ -19,6 +18,7 @@ from homeassistant.components import (
|
||||
light,
|
||||
media_player,
|
||||
number,
|
||||
remote,
|
||||
timer,
|
||||
vacuum,
|
||||
valve,
|
||||
@@ -439,6 +439,8 @@ class AlexaPowerController(AlexaCapability):
|
||||
is_on = self.entity.state == fan.STATE_ON
|
||||
elif self.entity.domain == humidifier.DOMAIN:
|
||||
is_on = self.entity.state == humidifier.STATE_ON
|
||||
elif self.entity.domain == remote.DOMAIN:
|
||||
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
|
||||
elif self.entity.domain == vacuum.DOMAIN:
|
||||
is_on = self.entity.state == vacuum.STATE_CLEANING
|
||||
elif self.entity.domain == timer.DOMAIN:
|
||||
@@ -1436,6 +1438,12 @@ class AlexaModeController(AlexaCapability):
|
||||
if mode in modes:
|
||||
return f"{humidifier.ATTR_MODE}.{mode}"
|
||||
|
||||
# Remote Activity
|
||||
if self.instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
|
||||
activity = self.entity.attributes.get(remote.ATTR_CURRENT_ACTIVITY, None)
|
||||
if activity in self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST, []):
|
||||
return f"{remote.ATTR_ACTIVITY}.{activity}"
|
||||
|
||||
# Water heater operation mode
|
||||
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
|
||||
operation_mode = self.entity.attributes.get(
|
||||
@@ -1550,6 +1558,24 @@ class AlexaModeController(AlexaCapability):
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Remote Resource
|
||||
if self.instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
|
||||
# Use the mode controller for a remote because the input controller
|
||||
# only allows a preset of names as an input.
|
||||
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
|
||||
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
|
||||
for activity in activities:
|
||||
self._resource.add_mode(
|
||||
f"{remote.ATTR_ACTIVITY}.{activity}", [activity]
|
||||
)
|
||||
# Remotes with a single activity completely break Alexa discovery, add a
|
||||
# fake activity to the mode controller (see issue #53832).
|
||||
if len(activities) == 1:
|
||||
self._resource.add_mode(
|
||||
f"{remote.ATTR_ACTIVITY}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Cover Position Resources
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
self._resource = AlexaModeResource(
|
||||
|
||||
@@ -88,7 +88,7 @@ API_THERMOSTAT_MODES_CUSTOM = {
|
||||
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
|
||||
|
||||
# AlexaModeController does not like a single mode for the fan preset or humidifier mode,
|
||||
# we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode
|
||||
# we add PRESET_MODE_NA if a fan / humidifier / remote has only one preset_mode
|
||||
PRESET_MODE_NA = "-"
|
||||
|
||||
STORAGE_ACCESS_TOKEN = "access_token"
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Generator, Iterable
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from typing_extensions import Generator
|
||||
|
||||
from homeassistant.components import (
|
||||
alarm_control_panel,
|
||||
alert,
|
||||
@@ -29,6 +27,7 @@ from homeassistant.components import (
|
||||
lock,
|
||||
media_player,
|
||||
number,
|
||||
remote,
|
||||
scene,
|
||||
script,
|
||||
sensor,
|
||||
@@ -198,6 +197,10 @@ class DisplayCategory:
|
||||
# Indicates a device that prints.
|
||||
PRINTER = "PRINTER"
|
||||
|
||||
# Indicates a decive that support stateless events,
|
||||
# such as remote switches and smart buttons.
|
||||
REMOTE = "REMOTE"
|
||||
|
||||
# Indicates a network router.
|
||||
ROUTER = "ROUTER"
|
||||
|
||||
@@ -647,6 +650,24 @@ class FanCapabilities(AlexaEntity):
|
||||
yield Alexa(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(remote.DOMAIN)
|
||||
class RemoteCapabilities(AlexaEntity):
|
||||
"""Class to represent Remote capabilities."""
|
||||
|
||||
def default_display_categories(self) -> list[str]:
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.REMOTE]
|
||||
|
||||
def interfaces(self) -> Generator[AlexaCapability]:
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaPowerController(self.entity)
|
||||
yield AlexaModeController(
|
||||
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
|
||||
)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(humidifier.DOMAIN)
|
||||
class HumidifierCapabilities(AlexaEntity):
|
||||
"""Class to represent Humidifier capabilities."""
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.components import (
|
||||
light,
|
||||
media_player,
|
||||
number,
|
||||
remote,
|
||||
timer,
|
||||
vacuum,
|
||||
valve,
|
||||
@@ -185,6 +186,8 @@ async def async_api_turn_on(
|
||||
service = fan.SERVICE_TURN_ON
|
||||
elif domain == humidifier.DOMAIN:
|
||||
service = humidifier.SERVICE_TURN_ON
|
||||
elif domain == remote.DOMAIN:
|
||||
service = remote.SERVICE_TURN_ON
|
||||
elif domain == vacuum.DOMAIN:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if (
|
||||
@@ -234,6 +237,8 @@ async def async_api_turn_off(
|
||||
service = climate.SERVICE_TURN_OFF
|
||||
elif domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
elif domain == remote.DOMAIN:
|
||||
service = remote.SERVICE_TURN_OFF
|
||||
elif domain == humidifier.DOMAIN:
|
||||
service = humidifier.SERVICE_TURN_OFF
|
||||
elif domain == vacuum.DOMAIN:
|
||||
@@ -1200,6 +1205,17 @@ async def async_api_set_mode(
|
||||
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
# Remote Activity
|
||||
if instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
|
||||
activity = mode.split(".")[1]
|
||||
activities: list[str] | None = entity.attributes.get(remote.ATTR_ACTIVITY_LIST)
|
||||
if activity != PRESET_MODE_NA and activities and activity in activities:
|
||||
service = remote.SERVICE_TURN_ON
|
||||
data[remote.ATTR_ACTIVITY] = activity
|
||||
else:
|
||||
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
# Water heater operation mode
|
||||
elif instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
|
||||
operation_mode = mode.split(".")[1]
|
||||
|
||||
@@ -71,6 +71,18 @@ class AmberPriceSpikeBinarySensor(AmberPriceGridSensor):
|
||||
}
|
||||
|
||||
|
||||
class AmberDemandWindowBinarySensor(AmberPriceGridSensor):
|
||||
"""Sensor to show whether demand window is active."""
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
grid = self.coordinator.data["grid"]
|
||||
if "demand_window" in grid:
|
||||
return grid["demand_window"] # type: ignore[no-any-return]
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
@@ -83,6 +95,14 @@ async def async_setup_entry(
|
||||
key="price_spike",
|
||||
name=f"{entry.title} - Price Spike",
|
||||
)
|
||||
async_add_entities(
|
||||
[AmberPriceSpikeBinarySensor(coordinator, price_spike_description)]
|
||||
demand_window_description = BinarySensorEntityDescription(
|
||||
key="demand_window",
|
||||
name=f"{entry.title} - Demand Window",
|
||||
translation_key="demand_window",
|
||||
)
|
||||
async_add_entities(
|
||||
[
|
||||
AmberPriceSpikeBinarySensor(coordinator, price_spike_description),
|
||||
AmberDemandWindowBinarySensor(coordinator, demand_window_description),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -111,6 +111,9 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
|
||||
]
|
||||
result["grid"]["renewables"] = round(general[0].renewables)
|
||||
result["grid"]["price_spike"] = general[0].spike_status.value
|
||||
tariff_information = general[0].tariff_information
|
||||
if tariff_information:
|
||||
result["grid"]["demand_window"] = tariff_information.demand_window
|
||||
|
||||
controlled_load = [
|
||||
interval for interval in current if is_controlled_load(interval)
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
"renewables": {
|
||||
"default": "mdi:solar-power"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"demand_window": {
|
||||
"default": "mdi:meter-electric",
|
||||
"state": {
|
||||
"off": "mdi:meter-electric-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["amberelectric"],
|
||||
"requirements": ["amberelectric==1.1.0"]
|
||||
"requirements": ["amberelectric==1.1.1"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anova",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["anova_wifi"],
|
||||
"requirements": ["anova-wifi==0.14.0"]
|
||||
"requirements": ["anova-wifi==0.17.0"]
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_STATE_CHANGED,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import Event, EventStateChangedData, HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -121,7 +119,7 @@ class KafkaManager:
|
||||
state = event.data["new_state"]
|
||||
if (
|
||||
state is None
|
||||
or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE)
|
||||
or state.state == ""
|
||||
or not self._entities_filter(state.entity_id)
|
||||
):
|
||||
return None
|
||||
@@ -141,7 +139,8 @@ class KafkaManager:
|
||||
|
||||
async def write(self, event: Event[EventStateChangedData]) -> None:
|
||||
"""Write a binary payload to Kafka."""
|
||||
key = event.data["entity_id"].encode("utf-8")
|
||||
payload = self._encode_event(event)
|
||||
|
||||
if payload:
|
||||
await self._producer.send_and_wait(self._topic, payload)
|
||||
await self._producer.send_and_wait(self._topic, payload, key)
|
||||
|
||||
@@ -17,6 +17,7 @@ from .coordinator import AprilaireCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.CLIMATE,
|
||||
Platform.HUMIDIFIER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
@@ -62,7 +62,7 @@ class AprilaireConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(title="Aprilaire", data=user_input)
|
||||
return self.async_create_entry(title="AprilAire", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
|
||||
194
homeassistant/components/aprilaire/humidifier.py
Normal file
194
homeassistant/components/aprilaire/humidifier.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""The Aprilaire humidifier component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from pyaprilaire.const import Attribute
|
||||
|
||||
from homeassistant.components.humidifier import (
|
||||
HumidifierAction,
|
||||
HumidifierDeviceClass,
|
||||
HumidifierEntity,
|
||||
HumidifierEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AprilaireCoordinator
|
||||
from .entity import BaseAprilaireEntity
|
||||
|
||||
HUMIDIFIER_ACTION_MAP: dict[StateType, HumidifierAction] = {
|
||||
0: HumidifierAction.IDLE,
|
||||
1: HumidifierAction.IDLE,
|
||||
2: HumidifierAction.HUMIDIFYING,
|
||||
3: HumidifierAction.OFF,
|
||||
}
|
||||
|
||||
DEHUMIDIFIER_ACTION_MAP: dict[StateType, HumidifierAction] = {
|
||||
0: HumidifierAction.IDLE,
|
||||
1: HumidifierAction.IDLE,
|
||||
2: HumidifierAction.DRYING,
|
||||
3: HumidifierAction.DRYING,
|
||||
4: HumidifierAction.OFF,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Aprilaire humidifier devices."""
|
||||
|
||||
coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id]
|
||||
|
||||
assert config_entry.unique_id is not None
|
||||
|
||||
descriptions: list[AprilaireHumidifierDescription] = []
|
||||
|
||||
if coordinator.data.get(Attribute.HUMIDIFICATION_AVAILABLE) in (0, 1, 2):
|
||||
descriptions.append(
|
||||
AprilaireHumidifierDescription(
|
||||
key="humidifier",
|
||||
translation_key="humidifier",
|
||||
device_class=HumidifierDeviceClass.HUMIDIFIER,
|
||||
action_key=Attribute.HUMIDIFICATION_STATUS,
|
||||
action_map=HUMIDIFIER_ACTION_MAP,
|
||||
current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE,
|
||||
target_humidity_key=Attribute.HUMIDIFICATION_SETPOINT,
|
||||
min_humidity=10,
|
||||
max_humidity=50,
|
||||
default_humidity=30,
|
||||
set_humidity_fn=coordinator.client.set_humidification_setpoint,
|
||||
)
|
||||
)
|
||||
|
||||
if coordinator.data.get(Attribute.DEHUMIDIFICATION_AVAILABLE) in (0, 1):
|
||||
descriptions.append(
|
||||
AprilaireHumidifierDescription(
|
||||
key="dehumidifier",
|
||||
translation_key="dehumidifier",
|
||||
device_class=HumidifierDeviceClass.DEHUMIDIFIER,
|
||||
action_key=Attribute.DEHUMIDIFICATION_STATUS,
|
||||
action_map=DEHUMIDIFIER_ACTION_MAP,
|
||||
current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE,
|
||||
target_humidity_key=Attribute.DEHUMIDIFICATION_SETPOINT,
|
||||
min_humidity=40,
|
||||
max_humidity=90,
|
||||
default_humidity=60,
|
||||
set_humidity_fn=coordinator.client.set_dehumidification_setpoint,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
AprilaireHumidifierEntity(coordinator, description, config_entry.unique_id)
|
||||
for description in descriptions
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AprilaireHumidifierDescription(HumidifierEntityDescription):
|
||||
"""Class describing Aprilaire humidifier entities."""
|
||||
|
||||
action_key: str
|
||||
action_map: dict[StateType, HumidifierAction]
|
||||
current_humidity_key: str
|
||||
target_humidity_key: str
|
||||
min_humidity: int
|
||||
max_humidity: int
|
||||
default_humidity: int
|
||||
set_humidity_fn: Callable[[int], Awaitable]
|
||||
|
||||
|
||||
class AprilaireHumidifierEntity(BaseAprilaireEntity, HumidifierEntity):
|
||||
"""Base humidity entity for Aprilaire."""
|
||||
|
||||
entity_description: AprilaireHumidifierDescription
|
||||
last_target_humidity: int | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AprilaireCoordinator,
|
||||
description: AprilaireHumidifierDescription,
|
||||
unique_id: str,
|
||||
) -> None:
|
||||
"""Initialize a select for an Aprilaire device."""
|
||||
|
||||
self.entity_description = description
|
||||
|
||||
super().__init__(coordinator, unique_id)
|
||||
|
||||
@property
|
||||
def action(self) -> HumidifierAction | None:
|
||||
"""Get the current action."""
|
||||
|
||||
action = self.coordinator.data.get(self.entity_description.action_key)
|
||||
|
||||
return self.entity_description.action_map.get(action, HumidifierAction.OFF)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the humidifier is on."""
|
||||
|
||||
return self.target_humidity is not None and self.target_humidity > 0
|
||||
|
||||
@property
|
||||
def current_humidity(self) -> float | None:
|
||||
"""Get the current humidity."""
|
||||
|
||||
return cast(
|
||||
float,
|
||||
self.coordinator.data.get(self.entity_description.current_humidity_key),
|
||||
)
|
||||
|
||||
@property
|
||||
def target_humidity(self) -> float | None:
|
||||
"""Get the target humidity."""
|
||||
|
||||
target_humidity = cast(
|
||||
float,
|
||||
self.coordinator.data.get(self.entity_description.target_humidity_key),
|
||||
)
|
||||
|
||||
if target_humidity is not None and target_humidity > 0:
|
||||
self.last_target_humidity = int(target_humidity)
|
||||
|
||||
return target_humidity
|
||||
|
||||
@property
|
||||
def min_humidity(self) -> float:
|
||||
"""Return the minimum humidity."""
|
||||
|
||||
return self.entity_description.min_humidity
|
||||
|
||||
@property
|
||||
def max_humidity(self) -> float:
|
||||
"""Return the maximum humidity."""
|
||||
|
||||
return self.entity_description.max_humidity
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set the humidity."""
|
||||
|
||||
await self.entity_description.set_humidity_fn(humidity)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
|
||||
if self.last_target_humidity is None or self.last_target_humidity == 0:
|
||||
target_humidity = self.entity_description.default_humidity
|
||||
else:
|
||||
target_humidity = self.last_target_humidity
|
||||
|
||||
await self.entity_description.set_humidity_fn(target_humidity)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
|
||||
await self.entity_description.set_humidity_fn(0)
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "aprilaire",
|
||||
"name": "Aprilaire",
|
||||
"name": "AprilAire",
|
||||
"codeowners": ["@chamberlain2007"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aprilaire",
|
||||
|
||||
@@ -24,6 +24,14 @@
|
||||
"name": "Thermostat"
|
||||
}
|
||||
},
|
||||
"humidifier": {
|
||||
"humidifier": {
|
||||
"name": "[%key:component::humidifier::title%]"
|
||||
},
|
||||
"dehumidifier": {
|
||||
"name": "[%key:component::humidifier::entity_component::dehumidifier::name%]"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"air_cleaning_event": {
|
||||
"name": "Air cleaning event",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioaquacell"],
|
||||
"requirements": ["aioaquacell==0.1.7"]
|
||||
"requirements": ["aioaquacell==0.1.8"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import array
|
||||
import asyncio
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import AsyncIterable, Callable, Iterable
|
||||
from collections.abc import AsyncGenerator, AsyncIterable, Callable
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
@@ -16,7 +16,6 @@ import time
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, cast
|
||||
import wave
|
||||
|
||||
from typing_extensions import AsyncGenerator
|
||||
import voluptuous as vol
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -116,10 +115,13 @@ AUDIO_PROCESSOR_SAMPLES: Final = 160 # 10 ms @ 16 Khz
|
||||
AUDIO_PROCESSOR_BYTES: Final = AUDIO_PROCESSOR_SAMPLES * 2 # 16-bit samples
|
||||
|
||||
|
||||
async def _async_resolve_default_pipeline_settings(
|
||||
@callback
|
||||
def _async_resolve_default_pipeline_settings(
|
||||
hass: HomeAssistant,
|
||||
stt_engine_id: str | None,
|
||||
tts_engine_id: str | None,
|
||||
*,
|
||||
conversation_engine_id: str | None = None,
|
||||
stt_engine_id: str | None = None,
|
||||
tts_engine_id: str | None = None,
|
||||
pipeline_name: str,
|
||||
) -> dict[str, str | None]:
|
||||
"""Resolve settings for a default pipeline.
|
||||
@@ -137,12 +139,13 @@ async def _async_resolve_default_pipeline_settings(
|
||||
wake_word_entity = None
|
||||
wake_word_id = None
|
||||
|
||||
if conversation_engine_id is None:
|
||||
conversation_engine_id = conversation.HOME_ASSISTANT_AGENT
|
||||
|
||||
# Find a matching language supported by the Home Assistant conversation agent
|
||||
conversation_languages = language_util.matches(
|
||||
hass.config.language,
|
||||
await conversation.async_get_conversation_languages(
|
||||
hass, conversation.HOME_ASSISTANT_AGENT
|
||||
),
|
||||
conversation.async_get_conversation_languages(hass, conversation_engine_id),
|
||||
country=hass.config.country,
|
||||
)
|
||||
if conversation_languages:
|
||||
@@ -201,7 +204,7 @@ async def _async_resolve_default_pipeline_settings(
|
||||
tts_engine_id = None
|
||||
|
||||
return {
|
||||
"conversation_engine": conversation.HOME_ASSISTANT_AGENT,
|
||||
"conversation_engine": conversation_engine_id,
|
||||
"conversation_language": conversation_language,
|
||||
"language": hass.config.language,
|
||||
"name": pipeline_name,
|
||||
@@ -223,8 +226,8 @@ async def _async_create_default_pipeline(
|
||||
The default pipeline will use the homeassistant conversation agent and the
|
||||
default stt / tts engines.
|
||||
"""
|
||||
pipeline_settings = await _async_resolve_default_pipeline_settings(
|
||||
hass, stt_engine_id=None, tts_engine_id=None, pipeline_name="Home Assistant"
|
||||
pipeline_settings = _async_resolve_default_pipeline_settings(
|
||||
hass, pipeline_name="Home Assistant"
|
||||
)
|
||||
return await pipeline_store.async_create_item(pipeline_settings)
|
||||
|
||||
@@ -242,8 +245,11 @@ async def async_create_default_pipeline(
|
||||
"""
|
||||
pipeline_data: PipelineData = hass.data[DOMAIN]
|
||||
pipeline_store = pipeline_data.pipeline_store
|
||||
pipeline_settings = await _async_resolve_default_pipeline_settings(
|
||||
hass, stt_engine_id, tts_engine_id, pipeline_name=pipeline_name
|
||||
pipeline_settings = _async_resolve_default_pipeline_settings(
|
||||
hass,
|
||||
stt_engine_id=stt_engine_id,
|
||||
tts_engine_id=tts_engine_id,
|
||||
pipeline_name=pipeline_name,
|
||||
)
|
||||
if (
|
||||
pipeline_settings["stt_engine"] != stt_engine_id
|
||||
@@ -253,6 +259,22 @@ async def async_create_default_pipeline(
|
||||
return await pipeline_store.async_create_item(pipeline_settings)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_pipeline_from_conversation_entity(
|
||||
hass: HomeAssistant, entity_id: str
|
||||
) -> Pipeline:
|
||||
"""Get a pipeline by conversation entity ID."""
|
||||
entity = hass.states.get(entity_id)
|
||||
settings = _async_resolve_default_pipeline_settings(
|
||||
hass,
|
||||
pipeline_name=entity.name if entity else entity_id,
|
||||
conversation_engine_id=entity_id,
|
||||
)
|
||||
settings["id"] = entity_id
|
||||
|
||||
return Pipeline.from_json(settings)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_pipeline(hass: HomeAssistant, pipeline_id: str | None = None) -> Pipeline:
|
||||
"""Get a pipeline by id or the preferred pipeline."""
|
||||
@@ -262,6 +284,9 @@ def async_get_pipeline(hass: HomeAssistant, pipeline_id: str | None = None) -> P
|
||||
# A pipeline was not specified, use the preferred one
|
||||
pipeline_id = pipeline_data.pipeline_store.async_get_preferred_item()
|
||||
|
||||
if pipeline_id.startswith("conversation."):
|
||||
return _async_get_pipeline_from_conversation_entity(hass, pipeline_id)
|
||||
|
||||
pipeline = pipeline_data.pipeline_store.data.get(pipeline_id)
|
||||
|
||||
# If invalid pipeline ID was specified
|
||||
@@ -274,11 +299,11 @@ def async_get_pipeline(hass: HomeAssistant, pipeline_id: str | None = None) -> P
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_pipelines(hass: HomeAssistant) -> Iterable[Pipeline]:
|
||||
def async_get_pipelines(hass: HomeAssistant) -> list[Pipeline]:
|
||||
"""Get all pipelines."""
|
||||
pipeline_data: PipelineData = hass.data[DOMAIN]
|
||||
|
||||
return pipeline_data.pipeline_store.data.values()
|
||||
return list(pipeline_data.pipeline_store.data.values())
|
||||
|
||||
|
||||
async def async_update_pipeline(
|
||||
@@ -304,21 +329,25 @@ async def async_update_pipeline(
|
||||
updates.pop("id")
|
||||
# Refactor this once we bump to Python 3.12
|
||||
# and have https://peps.python.org/pep-0692/
|
||||
for key, val in (
|
||||
("conversation_engine", conversation_engine),
|
||||
("conversation_language", conversation_language),
|
||||
("language", language),
|
||||
("name", name),
|
||||
("stt_engine", stt_engine),
|
||||
("stt_language", stt_language),
|
||||
("tts_engine", tts_engine),
|
||||
("tts_language", tts_language),
|
||||
("tts_voice", tts_voice),
|
||||
("wake_word_entity", wake_word_entity),
|
||||
("wake_word_id", wake_word_id),
|
||||
):
|
||||
if val is not UNDEFINED:
|
||||
updates[key] = val
|
||||
updates.update(
|
||||
{
|
||||
key: val
|
||||
for key, val in (
|
||||
("conversation_engine", conversation_engine),
|
||||
("conversation_language", conversation_language),
|
||||
("language", language),
|
||||
("name", name),
|
||||
("stt_engine", stt_engine),
|
||||
("stt_language", stt_language),
|
||||
("tts_engine", tts_engine),
|
||||
("tts_language", tts_language),
|
||||
("tts_voice", tts_voice),
|
||||
("wake_word_entity", wake_word_entity),
|
||||
("wake_word_id", wake_word_id),
|
||||
)
|
||||
if val is not UNDEFINED
|
||||
}
|
||||
)
|
||||
|
||||
await pipeline_data.pipeline_store.async_update_item(pipeline.id, updates)
|
||||
|
||||
@@ -855,6 +884,15 @@ class PipelineRun:
|
||||
stream: AsyncIterable[ProcessedAudioChunk],
|
||||
) -> str:
|
||||
"""Run speech-to-text portion of pipeline. Returns the spoken text."""
|
||||
# Create a background task to prepare the conversation agent
|
||||
if self.end_stage >= PipelineStage.INTENT:
|
||||
self.hass.async_create_background_task(
|
||||
conversation.async_prepare_agent(
|
||||
self.hass, self.intent_agent, self.language
|
||||
),
|
||||
f"prepare conversation agent {self.intent_agent}",
|
||||
)
|
||||
|
||||
if isinstance(self.stt_provider, stt.Provider):
|
||||
engine = self.stt_provider.name
|
||||
else:
|
||||
@@ -957,8 +995,6 @@ class PipelineRun:
|
||||
"""Prepare recognizing an intent."""
|
||||
agent_info = conversation.async_get_agent_info(
|
||||
self.hass,
|
||||
# If no conversation engine is set, use the Home Assistant agent
|
||||
# (the conversation integration default is currently the last one set)
|
||||
self.pipeline.conversation_engine or conversation.HOME_ASSISTANT_AGENT,
|
||||
)
|
||||
|
||||
@@ -1653,6 +1689,12 @@ class PipelineStorageCollectionWebsocket(
|
||||
if item_id is None:
|
||||
item_id = self.storage_collection.async_get_preferred_item()
|
||||
|
||||
if item_id.startswith("conversation.") and hass.states.get(item_id):
|
||||
connection.send_result(
|
||||
msg["id"], _async_get_pipeline_from_conversation_entity(hass, item_id)
|
||||
)
|
||||
return
|
||||
|
||||
if item_id not in self.storage_collection.data:
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
@@ -1671,7 +1713,7 @@ class PipelineStorageCollectionWebsocket(
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
"pipelines": self.storage_collection.async_items(),
|
||||
"pipelines": async_get_pipelines(hass),
|
||||
"preferred_pipeline": self.storage_collection.async_get_preferred_item(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -5,13 +5,12 @@ import asyncio
|
||||
# Suppressing disable=deprecated-module is needed for Python 3.11
|
||||
import audioop # pylint: disable=deprecated-module
|
||||
import base64
|
||||
from collections.abc import Callable
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
import contextlib
|
||||
import logging
|
||||
import math
|
||||
from typing import Any, Final
|
||||
|
||||
from typing_extensions import AsyncGenerator
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import conversation, stt, tts, websocket_api
|
||||
@@ -379,8 +378,8 @@ def websocket_get_run(
|
||||
vol.Required("type"): "assist_pipeline/language/list",
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def websocket_list_languages(
|
||||
@callback
|
||||
def websocket_list_languages(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.connection.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
@@ -390,7 +389,7 @@ async def websocket_list_languages(
|
||||
This will return a list of languages which are supported by at least one stt, tts
|
||||
and conversation engine respectively.
|
||||
"""
|
||||
conv_language_tags = await conversation.async_get_conversation_languages(hass)
|
||||
conv_language_tags = conversation.async_get_conversation_languages(hass)
|
||||
stt_language_tags = stt.async_get_speech_to_text_languages(hass)
|
||||
tts_language_tags = tts.async_get_text_to_speech_languages(hass)
|
||||
pipeline_languages: set[str] | None = None
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from path import Path
|
||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
|
||||
@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, Activity, ActivityType
|
||||
from yalexs.activity import Activity, ActivityType
|
||||
from yalexs.doorbell import DoorbellDetail
|
||||
from yalexs.lock import LockDetail, LockDoorStatus
|
||||
from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL
|
||||
@@ -26,67 +26,25 @@ from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from . import AugustConfigEntry, AugustData
|
||||
from .entity import AugustDescriptionEntity
|
||||
from .util import (
|
||||
retrieve_ding_activity,
|
||||
retrieve_doorbell_motion_activity,
|
||||
retrieve_online_state,
|
||||
retrieve_time_based_activity,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds())
|
||||
TIME_TO_RECHECK_DETECTION = timedelta(
|
||||
seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds() * 3
|
||||
)
|
||||
|
||||
|
||||
def _retrieve_online_state(
|
||||
data: AugustData, detail: DoorbellDetail | LockDetail
|
||||
) -> bool:
|
||||
"""Get the latest state of the sensor."""
|
||||
# The doorbell will go into standby mode when there is no motion
|
||||
# for a short while. It will wake by itself when needed so we need
|
||||
# to consider is available or we will not report motion or dings
|
||||
if isinstance(detail, DoorbellDetail):
|
||||
return detail.is_online or detail.is_standby
|
||||
return detail.bridge_is_online
|
||||
|
||||
|
||||
def _retrieve_time_based_state(
|
||||
activities: set[ActivityType], data: AugustData, detail: DoorbellDetail
|
||||
) -> bool:
|
||||
"""Get the latest state of the sensor."""
|
||||
stream = data.activity_stream
|
||||
if latest := stream.get_latest_device_activity(detail.device_id, activities):
|
||||
return _activity_time_based_state(latest)
|
||||
return False
|
||||
|
||||
|
||||
_RING_ACTIVITIES = {ActivityType.DOORBELL_DING}
|
||||
|
||||
|
||||
def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail | LockDetail) -> bool:
|
||||
stream = data.activity_stream
|
||||
latest = stream.get_latest_device_activity(detail.device_id, _RING_ACTIVITIES)
|
||||
if latest is None or (
|
||||
data.push_updates_connected and latest.action == ACTION_DOORBELL_CALL_MISSED
|
||||
):
|
||||
return False
|
||||
return _activity_time_based_state(latest)
|
||||
|
||||
|
||||
def _activity_time_based_state(latest: Activity) -> bool:
|
||||
"""Get the latest state of the sensor."""
|
||||
start = latest.activity_start_time
|
||||
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
|
||||
return start <= _native_datetime() <= end
|
||||
|
||||
|
||||
def _native_datetime() -> datetime:
|
||||
"""Return time in the format august uses without timezone."""
|
||||
return datetime.now()
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AugustDoorbellBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes August binary_sensor entity."""
|
||||
|
||||
value_fn: Callable[[AugustData, DoorbellDetail], bool]
|
||||
value_fn: Callable[[AugustData, DoorbellDetail | LockDetail], Activity | None]
|
||||
is_time_based: bool
|
||||
|
||||
|
||||
@@ -99,14 +57,14 @@ SENSOR_TYPES_VIDEO_DOORBELL = (
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="motion",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
value_fn=partial(_retrieve_time_based_state, {ActivityType.DOORBELL_MOTION}),
|
||||
value_fn=retrieve_doorbell_motion_activity,
|
||||
is_time_based=True,
|
||||
),
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="image capture",
|
||||
translation_key="image_capture",
|
||||
value_fn=partial(
|
||||
_retrieve_time_based_state, {ActivityType.DOORBELL_IMAGE_CAPTURE}
|
||||
retrieve_time_based_activity, {ActivityType.DOORBELL_IMAGE_CAPTURE}
|
||||
),
|
||||
is_time_based=True,
|
||||
),
|
||||
@@ -114,7 +72,7 @@ SENSOR_TYPES_VIDEO_DOORBELL = (
|
||||
key="online",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_online_state,
|
||||
value_fn=retrieve_online_state,
|
||||
is_time_based=False,
|
||||
),
|
||||
)
|
||||
@@ -124,7 +82,7 @@ SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] =
|
||||
AugustDoorbellBinarySensorEntityDescription(
|
||||
key="ding",
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
value_fn=_retrieve_ding_state,
|
||||
value_fn=retrieve_ding_activity,
|
||||
is_time_based=True,
|
||||
),
|
||||
)
|
||||
@@ -189,10 +147,12 @@ class AugustDoorbellBinarySensor(AugustDescriptionEntity, BinarySensorEntity):
|
||||
def _update_from_data(self) -> None:
|
||||
"""Get the latest state of the sensor."""
|
||||
self._cancel_any_pending_updates()
|
||||
self._attr_is_on = self.entity_description.value_fn(self._data, self._detail)
|
||||
self._attr_is_on = bool(
|
||||
self.entity_description.value_fn(self._data, self._detail)
|
||||
)
|
||||
|
||||
if self.entity_description.is_time_based:
|
||||
self._attr_available = _retrieve_online_state(self._data, self._detail)
|
||||
self._attr_available = retrieve_online_state(self._data, self._detail)
|
||||
self._schedule_update_to_recheck_turn_off_sensor()
|
||||
else:
|
||||
self._attr_available = True
|
||||
|
||||
@@ -42,6 +42,7 @@ PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CAMERA,
|
||||
Platform.EVENT,
|
||||
Platform.LOCK,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
104
homeassistant/components/august/event.py
Normal file
104
homeassistant/components/august/event.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""Support for august events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from yalexs.activity import Activity
|
||||
from yalexs.doorbell import DoorbellDetail
|
||||
from yalexs.lock import LockDetail
|
||||
|
||||
from homeassistant.components.event import (
|
||||
EventDeviceClass,
|
||||
EventEntity,
|
||||
EventEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AugustConfigEntry, AugustData
|
||||
from .entity import AugustDescriptionEntity
|
||||
from .util import (
|
||||
retrieve_ding_activity,
|
||||
retrieve_doorbell_motion_activity,
|
||||
retrieve_online_state,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class AugustEventEntityDescription(EventEntityDescription):
|
||||
"""Describe august event entities."""
|
||||
|
||||
value_fn: Callable[[AugustData, DoorbellDetail | LockDetail], Activity | None]
|
||||
|
||||
|
||||
TYPES_VIDEO_DOORBELL: tuple[AugustEventEntityDescription, ...] = (
|
||||
AugustEventEntityDescription(
|
||||
key="motion",
|
||||
translation_key="motion",
|
||||
device_class=EventDeviceClass.MOTION,
|
||||
event_types=["motion"],
|
||||
value_fn=retrieve_doorbell_motion_activity,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
TYPES_DOORBELL: tuple[AugustEventEntityDescription, ...] = (
|
||||
AugustEventEntityDescription(
|
||||
key="doorbell",
|
||||
translation_key="doorbell",
|
||||
device_class=EventDeviceClass.DOORBELL,
|
||||
event_types=["ring"],
|
||||
value_fn=retrieve_ding_activity,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AugustConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the august event platform."""
|
||||
data = config_entry.runtime_data
|
||||
entities: list[AugustEventEntity] = []
|
||||
|
||||
for lock in data.locks:
|
||||
detail = data.get_device_detail(lock.device_id)
|
||||
if detail.doorbell:
|
||||
entities.extend(
|
||||
AugustEventEntity(data, lock, description)
|
||||
for description in TYPES_DOORBELL
|
||||
)
|
||||
|
||||
for doorbell in data.doorbells:
|
||||
entities.extend(
|
||||
AugustEventEntity(data, doorbell, description)
|
||||
for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AugustEventEntity(AugustDescriptionEntity, EventEntity):
|
||||
"""An august event entity."""
|
||||
|
||||
entity_description: AugustEventEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
_last_activity: Activity | None = None
|
||||
|
||||
@callback
|
||||
def _update_from_data(self) -> None:
|
||||
"""Update from data."""
|
||||
self._attr_available = retrieve_online_state(self._data, self._detail)
|
||||
current_activity = self.entity_description.value_fn(self._data, self._detail)
|
||||
if not current_activity or current_activity == self._last_activity:
|
||||
return
|
||||
self._last_activity = current_activity
|
||||
event_types = self.entity_description.event_types
|
||||
if TYPE_CHECKING:
|
||||
assert event_types is not None
|
||||
self._trigger_event(event_type=event_types[0])
|
||||
self.async_write_ha_state()
|
||||
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.3"]
|
||||
"requirements": ["yalexs==6.4.2", "yalexs-ble==2.4.3"]
|
||||
}
|
||||
|
||||
@@ -58,6 +58,26 @@
|
||||
"operator": {
|
||||
"name": "Operator"
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"doorbell": {
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"ring": "Ring"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"motion": "Motion"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
"""August util functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import socket
|
||||
|
||||
import aiohttp
|
||||
from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, Activity, ActivityType
|
||||
from yalexs.doorbell import DoorbellDetail
|
||||
from yalexs.lock import LockDetail
|
||||
from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from . import AugustData
|
||||
|
||||
TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds())
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_august_clientsession(hass: HomeAssistant) -> aiohttp.ClientSession:
|
||||
@@ -22,3 +34,60 @@ def async_create_august_clientsession(hass: HomeAssistant) -> aiohttp.ClientSess
|
||||
# we can allow IPv6 again
|
||||
#
|
||||
return aiohttp_client.async_create_clientsession(hass, family=socket.AF_INET)
|
||||
|
||||
|
||||
def retrieve_time_based_activity(
|
||||
activities: set[ActivityType], data: AugustData, detail: DoorbellDetail | LockDetail
|
||||
) -> Activity | None:
|
||||
"""Get the latest state of the sensor."""
|
||||
stream = data.activity_stream
|
||||
if latest := stream.get_latest_device_activity(detail.device_id, activities):
|
||||
return _activity_time_based(latest)
|
||||
return False
|
||||
|
||||
|
||||
_RING_ACTIVITIES = {ActivityType.DOORBELL_DING}
|
||||
|
||||
|
||||
def retrieve_ding_activity(
|
||||
data: AugustData, detail: DoorbellDetail | LockDetail
|
||||
) -> Activity | None:
|
||||
"""Get the ring/ding state."""
|
||||
stream = data.activity_stream
|
||||
latest = stream.get_latest_device_activity(detail.device_id, _RING_ACTIVITIES)
|
||||
if latest is None or (
|
||||
data.push_updates_connected and latest.action == ACTION_DOORBELL_CALL_MISSED
|
||||
):
|
||||
return None
|
||||
return _activity_time_based(latest)
|
||||
|
||||
|
||||
retrieve_doorbell_motion_activity = partial(
|
||||
retrieve_time_based_activity, {ActivityType.DOORBELL_MOTION}
|
||||
)
|
||||
|
||||
|
||||
def _activity_time_based(latest: Activity) -> Activity | None:
|
||||
"""Get the latest state of the sensor."""
|
||||
start = latest.activity_start_time
|
||||
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
|
||||
if start <= _native_datetime() <= end:
|
||||
return latest
|
||||
return None
|
||||
|
||||
|
||||
def _native_datetime() -> datetime:
|
||||
"""Return time in the format august uses without timezone."""
|
||||
return datetime.now()
|
||||
|
||||
|
||||
def retrieve_online_state(
|
||||
data: AugustData, detail: DoorbellDetail | LockDetail
|
||||
) -> bool:
|
||||
"""Get the latest state of the sensor."""
|
||||
# The doorbell will go into standby mode when there is no motion
|
||||
# for a short while. It will wake by itself when needed so we need
|
||||
# to consider is available or we will not report motion or dings
|
||||
if isinstance(detail, DoorbellDetail):
|
||||
return detail.is_online or detail.is_standby
|
||||
return detail.bridge_is_online
|
||||
|
||||
49
homeassistant/components/autarco/__init__.py
Normal file
49
homeassistant/components/autarco/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""The Autarco integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from autarco import Autarco
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import AutarcoDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
type AutarcoConfigEntry = ConfigEntry[list[AutarcoDataUpdateCoordinator]]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AutarcoConfigEntry) -> bool:
|
||||
"""Set up Autarco from a config entry."""
|
||||
client = Autarco(
|
||||
email=entry.data[CONF_EMAIL],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
account_sites = await client.get_account()
|
||||
|
||||
coordinators: list[AutarcoDataUpdateCoordinator] = [
|
||||
AutarcoDataUpdateCoordinator(hass, client, site) for site in account_sites
|
||||
]
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
coordinator.async_config_entry_first_refresh()
|
||||
for coordinator in coordinators
|
||||
]
|
||||
)
|
||||
|
||||
entry.runtime_data = coordinators
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AutarcoConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
57
homeassistant/components/autarco/config_flow.py
Normal file
57
homeassistant/components/autarco/config_flow.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Config flow for Autarco integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from autarco import Autarco, AutarcoAuthenticationError, AutarcoConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EMAIL): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AutarcoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Autarco."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
|
||||
client = Autarco(
|
||||
email=user_input[CONF_EMAIL],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
await client.get_account()
|
||||
except AutarcoAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except AutarcoConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_EMAIL],
|
||||
data={
|
||||
CONF_EMAIL: user_input[CONF_EMAIL],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
errors=errors,
|
||||
data_schema=DATA_SCHEMA,
|
||||
)
|
||||
13
homeassistant/components/autarco/const.py
Normal file
13
homeassistant/components/autarco/const.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Constants for the Autarco integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "autarco"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
SENSORS_SOLAR: Final = "solar"
|
||||
47
homeassistant/components/autarco/coordinator.py
Normal file
47
homeassistant/components/autarco/coordinator.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Coordinator for Autarco integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
from autarco import AccountSite, Autarco, Solar
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
||||
|
||||
|
||||
class AutarcoData(NamedTuple):
|
||||
"""Class for defining data in dict."""
|
||||
|
||||
solar: Solar
|
||||
|
||||
|
||||
class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[AutarcoData]):
|
||||
"""Class to manage fetching Autarco data from the API."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
client: Autarco,
|
||||
site: AccountSite,
|
||||
) -> None:
|
||||
"""Initialize global Autarco data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.client = client
|
||||
self.site = site
|
||||
|
||||
async def _async_update_data(self) -> AutarcoData:
|
||||
"""Fetch data from Autarco API."""
|
||||
return AutarcoData(
|
||||
solar=await self.client.get_solar(self.site.public_key),
|
||||
)
|
||||
33
homeassistant/components/autarco/diagnostics.py
Normal file
33
homeassistant/components/autarco/diagnostics.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Support for the Autarco diagnostics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AutarcoConfigEntry, AutarcoDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: AutarcoConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
autarco_data: list[AutarcoDataUpdateCoordinator] = config_entry.runtime_data
|
||||
|
||||
return {
|
||||
"sites_data": [
|
||||
{
|
||||
"id": coordinator.site.site_id,
|
||||
"name": coordinator.site.system_name,
|
||||
"health": coordinator.site.health,
|
||||
"solar": {
|
||||
"power_production": coordinator.data.solar.power_production,
|
||||
"energy_production_today": coordinator.data.solar.energy_production_today,
|
||||
"energy_production_month": coordinator.data.solar.energy_production_month,
|
||||
"energy_production_total": coordinator.data.solar.energy_production_total,
|
||||
},
|
||||
}
|
||||
for coordinator in autarco_data
|
||||
],
|
||||
}
|
||||
9
homeassistant/components/autarco/manifest.json
Normal file
9
homeassistant/components/autarco/manifest.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"domain": "autarco",
|
||||
"name": "Autarco",
|
||||
"codeowners": ["@klaasnicolaas"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/autarco",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["autarco==2.0.0"]
|
||||
}
|
||||
114
homeassistant/components/autarco/sensor.py
Normal file
114
homeassistant/components/autarco/sensor.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Support for Autarco sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from autarco import Solar
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import UnitOfEnergy, UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AutarcoConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AutarcoDataUpdateCoordinator
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AutarcoSolarSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Autarco sensor entity."""
|
||||
|
||||
state: Callable[[Solar], StateType]
|
||||
|
||||
|
||||
SENSORS_SOLAR: tuple[AutarcoSolarSensorEntityDescription, ...] = (
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="power_production",
|
||||
translation_key="power_production",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state=lambda solar: solar.power_production,
|
||||
),
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="energy_production_today",
|
||||
translation_key="energy_production_today",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state=lambda solar: solar.energy_production_today,
|
||||
),
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="energy_production_month",
|
||||
translation_key="energy_production_month",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state=lambda solar: solar.energy_production_month,
|
||||
),
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="energy_production_total",
|
||||
translation_key="energy_production_total",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state=lambda solar: solar.energy_production_total,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AutarcoConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Autarco sensors based on a config entry."""
|
||||
for coordinator in entry.runtime_data:
|
||||
async_add_entities(
|
||||
AutarcoSolarSensorEntity(
|
||||
coordinator=coordinator,
|
||||
description=description,
|
||||
)
|
||||
for description in SENSORS_SOLAR
|
||||
)
|
||||
|
||||
|
||||
class AutarcoSolarSensorEntity(
|
||||
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Defines an Autarco solar sensor."""
|
||||
|
||||
entity_description: AutarcoSolarSensorEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
coordinator: AutarcoDataUpdateCoordinator,
|
||||
description: AutarcoSolarSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Autarco sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.site.site_id}_solar_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{coordinator.site.site_id}_solar")},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer="Autarco",
|
||||
name="Solar",
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.state(self.coordinator.data.solar)
|
||||
40
homeassistant/components/autarco/strings.json
Normal file
40
homeassistant/components/autarco/strings.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Connect to your Autarco account to get information about your solar panels.",
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"email": "The email address of your Autarco account.",
|
||||
"password": "The password of your Autarco account."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"power_production": {
|
||||
"name": "Power production"
|
||||
},
|
||||
"energy_production_today": {
|
||||
"name": "Energy production today"
|
||||
},
|
||||
"energy_production_month": {
|
||||
"name": "Energy production month"
|
||||
},
|
||||
"energy_production_total": {
|
||||
"name": "Energy production total"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,7 +215,7 @@ def _prepare_result_json(
|
||||
data = result.copy()
|
||||
|
||||
if (schema := data["data_schema"]) is None:
|
||||
data["data_schema"] = []
|
||||
data["data_schema"] = [] # type: ignore[typeddict-item] # json result type
|
||||
else:
|
||||
data["data_schema"] = voluptuous_serialize.convert(schema)
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ def _prepare_result_json(
|
||||
data = result.copy()
|
||||
|
||||
if (schema := data["data_schema"]) is None:
|
||||
data["data_schema"] = []
|
||||
data["data_schema"] = [] # type: ignore[typeddict-item] # json result type
|
||||
else:
|
||||
data["data_schema"] = voluptuous_serialize.convert(schema)
|
||||
|
||||
|
||||
@@ -1048,29 +1048,32 @@ async def _async_process_config(
|
||||
automation_configs_with_id: dict[str, tuple[int, AutomationEntityConfig]] = {}
|
||||
automation_configs_without_id: list[tuple[int, AutomationEntityConfig]] = []
|
||||
|
||||
for config_idx, config in enumerate(automation_configs):
|
||||
if automation_id := config.config_block.get(CONF_ID):
|
||||
automation_configs_with_id[automation_id] = (config_idx, config)
|
||||
for config_idx, automation_config in enumerate(automation_configs):
|
||||
if automation_id := automation_config.config_block.get(CONF_ID):
|
||||
automation_configs_with_id[automation_id] = (
|
||||
config_idx,
|
||||
automation_config,
|
||||
)
|
||||
continue
|
||||
automation_configs_without_id.append((config_idx, config))
|
||||
automation_configs_without_id.append((config_idx, automation_config))
|
||||
|
||||
for automation_idx, automation in enumerate(automations):
|
||||
if automation.unique_id:
|
||||
if automation.unique_id not in automation_configs_with_id:
|
||||
continue
|
||||
config_idx, config = automation_configs_with_id.pop(
|
||||
config_idx, automation_config = automation_configs_with_id.pop(
|
||||
automation.unique_id
|
||||
)
|
||||
if automation_matches_config(automation, config):
|
||||
if automation_matches_config(automation, automation_config):
|
||||
automation_matches.add(automation_idx)
|
||||
config_matches.add(config_idx)
|
||||
continue
|
||||
|
||||
for config_idx, config in automation_configs_without_id:
|
||||
for config_idx, automation_config in automation_configs_without_id:
|
||||
if config_idx in config_matches:
|
||||
# Only allow an automation config to match at most once
|
||||
continue
|
||||
if automation_matches_config(automation, config):
|
||||
if automation_matches_config(automation, automation_config):
|
||||
automation_matches.add(automation_idx)
|
||||
config_matches.add(config_idx)
|
||||
# Only allow an automation to match at most once
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
from contextlib import suppress
|
||||
from enum import StrEnum
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
@@ -90,7 +90,7 @@ async def _async_validate_config_item( # noqa: C901
|
||||
def _humanize(err: Exception, config: ConfigType) -> str:
|
||||
"""Humanize vol.Invalid, stringify other exceptions."""
|
||||
if isinstance(err, vol.Invalid):
|
||||
return cast(str, humanize_error(config, err))
|
||||
return humanize_error(config, err)
|
||||
return str(err)
|
||||
|
||||
def _log_invalid_automation(
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Generator
|
||||
|
||||
from homeassistant.components.trace import (
|
||||
CONF_STORED_TRACES,
|
||||
ActionTrace,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["axis==61"],
|
||||
"requirements": ["axis==62"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -78,8 +78,9 @@ class AzureDevOpsDataUpdateCoordinator(DataUpdateCoordinator[AzureDevOpsData]):
|
||||
)
|
||||
if not self.client.authorized:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Could not authorize with Azure DevOps. You will need to update your"
|
||||
" token"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_failed",
|
||||
translation_placeholders={"title": self.title},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -62,5 +62,10 @@
|
||||
"name": "{definition_name} latest build url"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"authentication_failed": {
|
||||
"message": "Could not authorize with Azure DevOps for {title}. You will need to update your personal access token."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,27 +10,17 @@ from mozart_api.models import Source, SourceArray, SourceTypeEnum
|
||||
from homeassistant.components.media_player import MediaPlayerState, MediaType
|
||||
|
||||
|
||||
class BangOlufsenSource(StrEnum):
|
||||
"""Enum used for associating device source ids with friendly names. May not include all sources."""
|
||||
class BangOlufsenSource:
|
||||
"""Class used for associating device source ids with friendly names. May not include all sources."""
|
||||
|
||||
URI_STREAMER = "Audio Streamer"
|
||||
BLUETOOTH = "Bluetooth"
|
||||
AIR_PLAY = "AirPlay"
|
||||
CHROMECAST = "Chromecast built-in"
|
||||
SPOTIFY = "Spotify Connect"
|
||||
GENERATOR = "Tone Generator"
|
||||
LINE_IN = "Line-In"
|
||||
SPDIF = "Optical"
|
||||
NET_RADIO = "B&O Radio"
|
||||
LOCAL = "Local"
|
||||
DLNA = "DLNA"
|
||||
QPLAY = "QPlay"
|
||||
WPL = "Wireless Powerlink"
|
||||
PL = "Powerlink"
|
||||
TV = "TV"
|
||||
DEEZER = "Deezer"
|
||||
BEOLINK = "Networklink"
|
||||
TIDAL_CONNECT = "Tidal Connect"
|
||||
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
|
||||
BLUETOOTH: Final[Source] = Source(name="Bluetooth", id="bluetooth")
|
||||
CHROMECAST: Final[Source] = Source(name="Chromecast built-in", id="chromeCast")
|
||||
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
|
||||
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
|
||||
NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
|
||||
DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
|
||||
TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
|
||||
|
||||
|
||||
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
||||
@@ -62,6 +52,7 @@ class BangOlufsenMediaType(StrEnum):
|
||||
class BangOlufsenModel(StrEnum):
|
||||
"""Enum for compatible model names."""
|
||||
|
||||
BEOCONNECT_CORE = "Beoconnect Core"
|
||||
BEOLAB_8 = "BeoLab 8"
|
||||
BEOLAB_28 = "BeoLab 28"
|
||||
BEOSOUND_2 = "Beosound 2 3rd Gen"
|
||||
|
||||
@@ -344,8 +344,8 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
|
||||
# Check if source is line-in or optical and progress should be updated
|
||||
if self._source_change.id in (
|
||||
BangOlufsenSource.LINE_IN,
|
||||
BangOlufsenSource.SPDIF,
|
||||
BangOlufsenSource.LINE_IN.id,
|
||||
BangOlufsenSource.SPDIF.id,
|
||||
):
|
||||
self._playback_progress = PlaybackProgress(progress=0)
|
||||
|
||||
@@ -381,7 +381,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
def media_content_type(self) -> str:
|
||||
"""Return the current media type."""
|
||||
# Hard to determine content type
|
||||
if self.source == BangOlufsenSource.URI_STREAMER:
|
||||
if self._source_change.id == BangOlufsenSource.URI_STREAMER.id:
|
||||
return MediaType.URL
|
||||
return MediaType.MUSIC
|
||||
|
||||
@@ -398,9 +398,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def media_image_url(self) -> str | None:
|
||||
"""Return URL of the currently playing music."""
|
||||
if self._media_image:
|
||||
return self._media_image.url
|
||||
return None
|
||||
return self._media_image.url
|
||||
|
||||
@property
|
||||
def media_image_remotely_accessible(self) -> bool:
|
||||
@@ -439,33 +437,31 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
# Try to fix some of the source_change chromecast weirdness.
|
||||
if hasattr(self._playback_metadata, "title"):
|
||||
# source_change is chromecast but line in is selected.
|
||||
if self._playback_metadata.title == BangOlufsenSource.LINE_IN:
|
||||
return BangOlufsenSource.LINE_IN
|
||||
if self._playback_metadata.title == BangOlufsenSource.LINE_IN.name:
|
||||
return BangOlufsenSource.LINE_IN.name
|
||||
|
||||
# source_change is chromecast but bluetooth is selected.
|
||||
if self._playback_metadata.title == BangOlufsenSource.BLUETOOTH:
|
||||
return BangOlufsenSource.BLUETOOTH
|
||||
if self._playback_metadata.title == BangOlufsenSource.BLUETOOTH.name:
|
||||
return BangOlufsenSource.BLUETOOTH.name
|
||||
|
||||
# source_change is line in, bluetooth or optical but stale metadata is sent through the WebSocket,
|
||||
# And the source has not changed.
|
||||
if self._source_change.id in (
|
||||
BangOlufsenSource.BLUETOOTH,
|
||||
BangOlufsenSource.LINE_IN,
|
||||
BangOlufsenSource.SPDIF,
|
||||
BangOlufsenSource.BLUETOOTH.id,
|
||||
BangOlufsenSource.LINE_IN.id,
|
||||
BangOlufsenSource.SPDIF.id,
|
||||
):
|
||||
return BangOlufsenSource.CHROMECAST
|
||||
return BangOlufsenSource.CHROMECAST.name
|
||||
|
||||
# source_change is chromecast and there is metadata but no artwork. Bluetooth does support metadata but not artwork
|
||||
# So i assume that it is bluetooth and not chromecast
|
||||
if (
|
||||
hasattr(self._playback_metadata, "art")
|
||||
and self._playback_metadata.art is not None
|
||||
and len(self._playback_metadata.art) == 0
|
||||
and self._source_change.id == BangOlufsenSource.CHROMECAST.id
|
||||
):
|
||||
if (
|
||||
len(self._playback_metadata.art) == 0
|
||||
and self._source_change.name == BangOlufsenSource.BLUETOOTH
|
||||
):
|
||||
return BangOlufsenSource.BLUETOOTH
|
||||
return BangOlufsenSource.BLUETOOTH.name
|
||||
|
||||
return self._source_change.name
|
||||
|
||||
@@ -508,7 +504,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
|
||||
async def async_media_seek(self, position: float) -> None:
|
||||
"""Seek to position in ms."""
|
||||
if self.source == BangOlufsenSource.DEEZER:
|
||||
if self._source_change.id == BangOlufsenSource.DEEZER.id:
|
||||
await self._client.seek_to_position(position_ms=int(position * 1000))
|
||||
# Try to prevent the playback progress from bouncing in the UI.
|
||||
self._attr_media_position_updated_at = utcnow()
|
||||
|
||||
@@ -4,14 +4,13 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_DEVICE_ID, CONF_PIN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_SEND_PIN
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
|
||||
SERVICE_UPDATE_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -29,45 +28,6 @@ SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||
def setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Blink integration."""
|
||||
|
||||
def collect_coordinators(
|
||||
device_ids: list[str],
|
||||
) -> list[BlinkUpdateCoordinator]:
|
||||
config_entries: list[ConfigEntry] = []
|
||||
registry = dr.async_get(hass)
|
||||
for target in device_ids:
|
||||
device = registry.async_get(target)
|
||||
if device:
|
||||
device_entries: list[ConfigEntry] = []
|
||||
for entry_id in device.config_entries:
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if entry and entry.domain == DOMAIN:
|
||||
device_entries.append(entry)
|
||||
if not device_entries:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_device",
|
||||
translation_placeholders={"target": target, "domain": DOMAIN},
|
||||
)
|
||||
config_entries.extend(device_entries)
|
||||
else:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_not_found",
|
||||
translation_placeholders={"target": target},
|
||||
)
|
||||
|
||||
coordinators: list[BlinkUpdateCoordinator] = []
|
||||
for config_entry in config_entries:
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_loaded",
|
||||
translation_placeholders={"target": config_entry.title},
|
||||
)
|
||||
|
||||
coordinators.append(hass.data[DOMAIN][config_entry.entry_id])
|
||||
return coordinators
|
||||
|
||||
async def send_pin(call: ServiceCall):
|
||||
"""Call blink to send new pin."""
|
||||
for entry_id in call.data[ATTR_CONFIG_ENTRY_ID]:
|
||||
|
||||
@@ -99,8 +99,7 @@ class Blueprint:
|
||||
inputs = {}
|
||||
for key, value in self.data[CONF_BLUEPRINT][CONF_INPUT].items():
|
||||
if value and CONF_INPUT in value:
|
||||
for key, value in value[CONF_INPUT].items():
|
||||
inputs[key] = value
|
||||
inputs.update(dict(value[CONF_INPUT]))
|
||||
else:
|
||||
inputs[key] = value
|
||||
return inputs
|
||||
|
||||
@@ -15,11 +15,9 @@ from homeassistant.helpers.update_coordinator import (
|
||||
from .update_coordinator import BasePassiveBluetoothCoordinator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Generator
|
||||
import logging
|
||||
|
||||
from typing_extensions import Generator
|
||||
|
||||
from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak
|
||||
|
||||
_PassiveBluetoothDataUpdateCoordinatorT = TypeVar(
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from bimmer_connected.vehicle import MyBMWVehicle
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -17,11 +16,8 @@ from homeassistant.helpers import (
|
||||
entity_registry as er,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTR_VIN, ATTRIBUTION, CONF_READ_ONLY, DATA_HASS_CONFIG, DOMAIN
|
||||
from .const import ATTR_VIN, CONF_READ_ONLY, DOMAIN
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -55,13 +51,14 @@ PLATFORMS = [
|
||||
SERVICE_UPDATE_STATE = "update_state"
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the BMW Connected Drive component from configuration.yaml."""
|
||||
# Store full yaml config in data for platform.NOTIFY
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][DATA_HASS_CONFIG] = config
|
||||
type BMWConfigEntry = ConfigEntry[BMWData]
|
||||
|
||||
return True
|
||||
|
||||
@dataclass
|
||||
class BMWData:
|
||||
"""Class to store BMW runtime data."""
|
||||
|
||||
coordinator: BMWDataUpdateCoordinator
|
||||
|
||||
|
||||
@callback
|
||||
@@ -82,7 +79,7 @@ def _async_migrate_options_from_data_if_missing(
|
||||
|
||||
|
||||
async def _async_migrate_entries(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: BMWConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate old entry."""
|
||||
entity_registry = er.async_get(hass)
|
||||
@@ -90,8 +87,20 @@ async def _async_migrate_entries(
|
||||
@callback
|
||||
def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None:
|
||||
replacements = {
|
||||
"charging_level_hv": "remaining_battery_percent",
|
||||
"fuel_percent": "remaining_fuel_percent",
|
||||
"charging_level_hv": "fuel_and_battery.remaining_battery_percent",
|
||||
"fuel_percent": "fuel_and_battery.remaining_fuel_percent",
|
||||
"ac_current_limit": "charging_profile.ac_current_limit",
|
||||
"charging_start_time": "fuel_and_battery.charging_start_time",
|
||||
"charging_end_time": "fuel_and_battery.charging_end_time",
|
||||
"charging_status": "fuel_and_battery.charging_status",
|
||||
"charging_target": "fuel_and_battery.charging_target",
|
||||
"remaining_battery_percent": "fuel_and_battery.remaining_battery_percent",
|
||||
"remaining_range_total": "fuel_and_battery.remaining_range_total",
|
||||
"remaining_range_electric": "fuel_and_battery.remaining_range_electric",
|
||||
"remaining_range_fuel": "fuel_and_battery.remaining_range_fuel",
|
||||
"remaining_fuel": "fuel_and_battery.remaining_fuel",
|
||||
"remaining_fuel_percent": "fuel_and_battery.remaining_fuel_percent",
|
||||
"activity": "climate.activity",
|
||||
}
|
||||
if (key := entry.unique_id.split("-")[-1]) in replacements:
|
||||
new_unique_id = entry.unique_id.replace(key, replacements[key])
|
||||
@@ -134,8 +143,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = BMWData(coordinator)
|
||||
|
||||
# Set up all platforms except notify
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
@@ -150,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
Platform.NOTIFY,
|
||||
DOMAIN,
|
||||
{CONF_NAME: DOMAIN, CONF_ENTITY_ID: entry.entry_id},
|
||||
hass.data[DOMAIN][DATA_HASS_CONFIG],
|
||||
{},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -171,45 +179,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(
|
||||
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
|
||||
)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class BMWBaseEntity(CoordinatorEntity[BMWDataUpdateCoordinator]):
|
||||
"""Common base for BMW entities."""
|
||||
|
||||
coordinator: BMWDataUpdateCoordinator
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: BMWDataUpdateCoordinator,
|
||||
vehicle: MyBMWVehicle,
|
||||
) -> None:
|
||||
"""Initialize entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.vehicle = vehicle
|
||||
|
||||
self._attrs: dict[str, Any] = {
|
||||
"car": self.vehicle.name,
|
||||
"vin": self.vehicle.vin,
|
||||
}
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.vehicle.vin)},
|
||||
manufacturer=vehicle.brand.name,
|
||||
model=vehicle.name,
|
||||
name=vehicle.name,
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self._handle_coordinator_update()
|
||||
|
||||
@@ -17,14 +17,14 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.unit_system import UnitSystem
|
||||
|
||||
from . import BMWBaseEntity
|
||||
from .const import DOMAIN, UNIT_MAP
|
||||
from . import BMWConfigEntry
|
||||
from .const import UNIT_MAP
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
from .entity import BMWBaseEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -197,11 +197,11 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BMWConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the BMW binary sensors from config entry."""
|
||||
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
entities = [
|
||||
BMWBinarySensor(coordinator, vehicle, description, hass.config.units)
|
||||
@@ -241,9 +241,8 @@ class BMWBinarySensor(BMWBaseEntity, BinarySensorEntity):
|
||||
self._attr_is_on = self.entity_description.value_fn(self.vehicle)
|
||||
|
||||
if self.entity_description.attr_fn:
|
||||
self._attr_extra_state_attributes = dict(
|
||||
self._attrs,
|
||||
**self.entity_description.attr_fn(self.vehicle, self._unit_system),
|
||||
self._attr_extra_state_attributes = self.entity_description.attr_fn(
|
||||
self.vehicle, self._unit_system
|
||||
)
|
||||
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@@ -12,13 +12,12 @@ from bimmer_connected.vehicle import MyBMWVehicle
|
||||
from bimmer_connected.vehicle.remote_services import RemoteServiceStatus
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import BMWBaseEntity
|
||||
from .const import DOMAIN
|
||||
from . import BMWConfigEntry
|
||||
from .entity import BMWBaseEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
@@ -68,11 +67,11 @@ BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BMWConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the BMW buttons from config entry."""
|
||||
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
entities: list[BMWButton] = []
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||
|
||||
from . import DOMAIN
|
||||
from .const import CONF_ALLOWED_REGIONS, CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN
|
||||
@@ -28,7 +29,12 @@ DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_REGION): vol.In(CONF_ALLOWED_REGIONS),
|
||||
vol.Required(CONF_REGION): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=CONF_ALLOWED_REGIONS,
|
||||
translation_key="regions",
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from homeassistant.const import UnitOfLength, UnitOfVolume
|
||||
|
||||
DOMAIN = "bmw_connected_drive"
|
||||
ATTRIBUTION = "Data provided by MyBMW"
|
||||
|
||||
ATTR_DIRECTION = "direction"
|
||||
ATTR_VIN = "vin"
|
||||
|
||||
@@ -8,24 +8,24 @@ from typing import Any
|
||||
from bimmer_connected.vehicle import MyBMWVehicle
|
||||
|
||||
from homeassistant.components.device_tracker import SourceType, TrackerEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import BMWBaseEntity
|
||||
from .const import ATTR_DIRECTION, DOMAIN
|
||||
from . import BMWConfigEntry
|
||||
from .const import ATTR_DIRECTION
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
from .entity import BMWBaseEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BMWConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the MyBMW tracker from config entry."""
|
||||
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
entities: list[BMWDeviceTracker] = []
|
||||
|
||||
for vehicle in coordinator.account.vehicles:
|
||||
@@ -63,7 +63,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity):
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return entity specific state attributes."""
|
||||
return {**self._attrs, ATTR_DIRECTION: self.vehicle.vehicle_location.heading}
|
||||
return {ATTR_DIRECTION: self.vehicle.vehicle_location.heading}
|
||||
|
||||
@property
|
||||
def latitude(self) -> float | None:
|
||||
|
||||
@@ -9,17 +9,16 @@ from typing import TYPE_CHECKING, Any
|
||||
from bimmer_connected.utils import MyBMWJSONEncoder
|
||||
|
||||
from homeassistant.components.diagnostics.util import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .const import CONF_REFRESH_TOKEN, DOMAIN
|
||||
from . import BMWConfigEntry
|
||||
from .const import CONF_REFRESH_TOKEN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bimmer_connected.vehicle import MyBMWVehicle
|
||||
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
|
||||
TO_REDACT_INFO = [CONF_USERNAME, CONF_PASSWORD, CONF_REFRESH_TOKEN]
|
||||
TO_REDACT_DATA = [
|
||||
@@ -47,10 +46,10 @@ def vehicle_to_dict(vehicle: MyBMWVehicle | None) -> dict:
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: BMWConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
coordinator.account.config.log_responses = True
|
||||
await coordinator.account.get_vehicles(force_init=True)
|
||||
@@ -73,10 +72,10 @@ async def async_get_config_entry_diagnostics(
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry
|
||||
hass: HomeAssistant, config_entry: BMWConfigEntry, device: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
coordinator.account.config.log_responses = True
|
||||
await coordinator.account.get_vehicles(force_init=True)
|
||||
|
||||
40
homeassistant/components/bmw_connected_drive/entity.py
Normal file
40
homeassistant/components/bmw_connected_drive/entity.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Base for all BMW entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from bimmer_connected.vehicle import MyBMWVehicle
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
|
||||
|
||||
class BMWBaseEntity(CoordinatorEntity[BMWDataUpdateCoordinator]):
|
||||
"""Common base for BMW entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: BMWDataUpdateCoordinator,
|
||||
vehicle: MyBMWVehicle,
|
||||
) -> None:
|
||||
"""Initialize entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.vehicle = vehicle
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, vehicle.vin)},
|
||||
manufacturer=vehicle.brand.name,
|
||||
model=vehicle.name,
|
||||
name=vehicle.name,
|
||||
serial_number=vehicle.vin,
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self._handle_coordinator_update()
|
||||
@@ -10,14 +10,13 @@ from bimmer_connected.vehicle import MyBMWVehicle
|
||||
from bimmer_connected.vehicle.doors_windows import LockState
|
||||
|
||||
from homeassistant.components.lock import LockEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import BMWBaseEntity
|
||||
from .const import DOMAIN
|
||||
from . import BMWConfigEntry
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
from .entity import BMWBaseEntity
|
||||
|
||||
DOOR_LOCK_STATE = "door_lock_state"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -25,11 +24,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BMWConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the MyBMW lock from config entry."""
|
||||
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
if not coordinator.read_only:
|
||||
async_add_entities(
|
||||
@@ -97,8 +96,6 @@ class BMWLock(BMWBaseEntity, LockEntity):
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
_LOGGER.debug("Updating lock data of %s", self.vehicle.name)
|
||||
# Set default attributes
|
||||
self._attr_extra_state_attributes = self._attrs
|
||||
|
||||
# Only update the HA state machine if the vehicle reliably reports its lock state
|
||||
if self.door_lock_state_available:
|
||||
@@ -106,8 +103,8 @@ class BMWLock(BMWBaseEntity, LockEntity):
|
||||
LockState.LOCKED,
|
||||
LockState.SECURED,
|
||||
}
|
||||
self._attr_extra_state_attributes["door_lock_state"] = (
|
||||
self.vehicle.doors_and_windows.door_lock_state.value
|
||||
)
|
||||
self._attr_extra_state_attributes = {
|
||||
DOOR_LOCK_STATE: self.vehicle.doors_and_windows.door_lock_state.value
|
||||
}
|
||||
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["bimmer-connected[china]==0.15.3"]
|
||||
}
|
||||
|
||||
@@ -5,33 +5,35 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from bimmer_connected.models import MyBMWAPIError
|
||||
from bimmer_connected.models import MyBMWAPIError, PointOfInterest
|
||||
from bimmer_connected.vehicle import MyBMWVehicle
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_DATA,
|
||||
ATTR_TARGET,
|
||||
BaseNotificationService,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LOCATION,
|
||||
ATTR_LONGITUDE,
|
||||
ATTR_NAME,
|
||||
CONF_ENTITY_ID,
|
||||
)
|
||||
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
from . import DOMAIN, BMWConfigEntry
|
||||
|
||||
ATTR_LAT = "lat"
|
||||
ATTR_LOCATION_ATTRIBUTES = ["street", "city", "postal_code", "country"]
|
||||
ATTR_LON = "lon"
|
||||
ATTR_SUBJECT = "subject"
|
||||
ATTR_TEXT = "text"
|
||||
|
||||
POI_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_LATITUDE): cv.latitude,
|
||||
vol.Required(ATTR_LONGITUDE): cv.longitude,
|
||||
vol.Optional("street"): cv.string,
|
||||
vol.Optional("city"): cv.string,
|
||||
vol.Optional("postal_code"): cv.string,
|
||||
vol.Optional("country"): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,12 +44,16 @@ def get_service(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> BMWNotificationService:
|
||||
"""Get the BMW notification service."""
|
||||
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry: BMWConfigEntry | None = hass.config_entries.async_get_entry(
|
||||
(discovery_info or {})[CONF_ENTITY_ID]
|
||||
]
|
||||
)
|
||||
|
||||
targets = {}
|
||||
if not coordinator.read_only:
|
||||
if (
|
||||
config_entry
|
||||
and (coordinator := config_entry.runtime_data.coordinator)
|
||||
and not coordinator.read_only
|
||||
):
|
||||
targets.update({v.name: v for v in coordinator.account.vehicles})
|
||||
return BMWNotificationService(targets)
|
||||
|
||||
@@ -68,33 +74,34 @@ class BMWNotificationService(BaseNotificationService):
|
||||
|
||||
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message or POI to the car."""
|
||||
|
||||
try:
|
||||
# Verify data schema
|
||||
poi_data = kwargs.get(ATTR_DATA) or {}
|
||||
POI_SCHEMA(poi_data)
|
||||
|
||||
# Create the POI object
|
||||
poi = PointOfInterest(
|
||||
lat=poi_data.pop(ATTR_LATITUDE),
|
||||
lon=poi_data.pop(ATTR_LONGITUDE),
|
||||
name=(message or None),
|
||||
**poi_data,
|
||||
)
|
||||
|
||||
except (vol.Invalid, TypeError, ValueError) as ex:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_poi",
|
||||
translation_placeholders={
|
||||
"poi_exception": str(ex),
|
||||
},
|
||||
) from ex
|
||||
|
||||
for vehicle in kwargs[ATTR_TARGET]:
|
||||
vehicle = cast(MyBMWVehicle, vehicle)
|
||||
_LOGGER.debug("Sending message to %s", vehicle.name)
|
||||
|
||||
# Extract params from data dict
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
|
||||
# Check if message is a POI
|
||||
if data is not None and ATTR_LOCATION in data:
|
||||
location_dict = {
|
||||
ATTR_LAT: data[ATTR_LOCATION][ATTR_LATITUDE],
|
||||
ATTR_LON: data[ATTR_LOCATION][ATTR_LONGITUDE],
|
||||
ATTR_NAME: message,
|
||||
}
|
||||
# Update dictionary with additional attributes if available
|
||||
location_dict.update(
|
||||
{
|
||||
k: v
|
||||
for k, v in data[ATTR_LOCATION].items()
|
||||
if k in ATTR_LOCATION_ATTRIBUTES
|
||||
}
|
||||
)
|
||||
try:
|
||||
await vehicle.remote_services.trigger_send_poi(location_dict)
|
||||
except TypeError as ex:
|
||||
raise ValueError(str(ex)) from ex
|
||||
except MyBMWAPIError as ex:
|
||||
raise HomeAssistantError(ex) from ex
|
||||
else:
|
||||
raise ValueError(f"'data.{ATTR_LOCATION}' is required.")
|
||||
try:
|
||||
await vehicle.remote_services.trigger_send_poi(poi)
|
||||
except MyBMWAPIError as ex:
|
||||
raise HomeAssistantError(ex) from ex
|
||||
|
||||
@@ -14,14 +14,13 @@ from homeassistant.components.number import (
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import BMWBaseEntity
|
||||
from .const import DOMAIN
|
||||
from . import BMWConfigEntry
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
from .entity import BMWBaseEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -56,11 +55,11 @@ NUMBER_TYPES: list[BMWNumberEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BMWConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the MyBMW number from config entry."""
|
||||
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
entities: list[BMWNumber] = []
|
||||
|
||||
|
||||
@@ -10,15 +10,14 @@ from bimmer_connected.vehicle import MyBMWVehicle
|
||||
from bimmer_connected.vehicle.charging_profile import ChargingMode
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfElectricCurrent
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import BMWBaseEntity
|
||||
from .const import DOMAIN
|
||||
from . import BMWConfigEntry
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
from .entity import BMWBaseEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -63,11 +62,11 @@ SELECT_TYPES: tuple[BMWSelectEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BMWConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the MyBMW lock from config entry."""
|
||||
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
entities: list[BMWSelect] = []
|
||||
|
||||
|
||||
@@ -18,21 +18,21 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
STATE_UNKNOWN,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfLength,
|
||||
UnitOfPressure,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import BMWBaseEntity
|
||||
from .const import DOMAIN
|
||||
from . import BMWConfigEntry
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
from .entity import BMWBaseEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,11 +45,12 @@ class BMWSensorEntityDescription(SensorEntityDescription):
|
||||
is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled
|
||||
|
||||
|
||||
TIRES = ["front_left", "front_right", "rear_left", "rear_right"]
|
||||
|
||||
SENSOR_TYPES: list[BMWSensorEntityDescription] = [
|
||||
BMWSensorEntityDescription(
|
||||
key="ac_current_limit",
|
||||
key="charging_profile.ac_current_limit",
|
||||
translation_key="ac_current_limit",
|
||||
key_class="charging_profile",
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -57,41 +58,36 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="charging_start_time",
|
||||
key="fuel_and_battery.charging_start_time",
|
||||
translation_key="charging_start_time",
|
||||
key_class="fuel_and_battery",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_registry_enabled_default=False,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="charging_end_time",
|
||||
key="fuel_and_battery.charging_end_time",
|
||||
translation_key="charging_end_time",
|
||||
key_class="fuel_and_battery",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="charging_status",
|
||||
key="fuel_and_battery.charging_status",
|
||||
translation_key="charging_status",
|
||||
key_class="fuel_and_battery",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[s.value.lower() for s in ChargingState if s != ChargingState.UNKNOWN],
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="charging_target",
|
||||
key="fuel_and_battery.charging_target",
|
||||
translation_key="charging_target",
|
||||
key_class="fuel_and_battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
suggested_display_precision=0,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="remaining_battery_percent",
|
||||
key="fuel_and_battery.remaining_battery_percent",
|
||||
translation_key="remaining_battery_percent",
|
||||
key_class="fuel_and_battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -107,18 +103,16 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="remaining_range_total",
|
||||
key="fuel_and_battery.remaining_range_total",
|
||||
translation_key="remaining_range_total",
|
||||
key_class="fuel_and_battery",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="remaining_range_electric",
|
||||
key="fuel_and_battery.remaining_range_electric",
|
||||
translation_key="remaining_range_electric",
|
||||
key_class="fuel_and_battery",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -126,9 +120,8 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="remaining_range_fuel",
|
||||
key="fuel_and_battery.remaining_range_fuel",
|
||||
translation_key="remaining_range_fuel",
|
||||
key_class="fuel_and_battery",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -136,9 +129,8 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="remaining_fuel",
|
||||
key="fuel_and_battery.remaining_fuel",
|
||||
translation_key="remaining_fuel",
|
||||
key_class="fuel_and_battery",
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
native_unit_of_measurement=UnitOfVolume.LITERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -146,18 +138,16 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="remaining_fuel_percent",
|
||||
key="fuel_and_battery.remaining_fuel_percent",
|
||||
translation_key="remaining_fuel_percent",
|
||||
key_class="fuel_and_battery",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain,
|
||||
),
|
||||
BMWSensorEntityDescription(
|
||||
key="activity",
|
||||
key="climate.activity",
|
||||
translation_key="climate_status",
|
||||
key_class="climate",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
s.value.lower()
|
||||
@@ -166,16 +156,43 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
|
||||
],
|
||||
is_available=lambda v: v.is_remote_climate_stop_enabled,
|
||||
),
|
||||
*[
|
||||
BMWSensorEntityDescription(
|
||||
key=f"tires.{tire}.current_pressure",
|
||||
translation_key=f"{tire}_current_pressure",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.KPA,
|
||||
suggested_unit_of_measurement=UnitOfPressure.BAR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.tires is not None,
|
||||
)
|
||||
for tire in TIRES
|
||||
],
|
||||
*[
|
||||
BMWSensorEntityDescription(
|
||||
key=f"tires.{tire}.target_pressure",
|
||||
translation_key=f"{tire}_target_pressure",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.KPA,
|
||||
suggested_unit_of_measurement=UnitOfPressure.BAR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
entity_registry_enabled_default=False,
|
||||
is_available=lambda v: v.is_lsc_enabled and v.tires is not None,
|
||||
)
|
||||
for tire in TIRES
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BMWConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the MyBMW sensors from config entry."""
|
||||
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
entities = [
|
||||
BMWSensor(coordinator, vehicle, description)
|
||||
@@ -209,13 +226,12 @@ class BMWSensor(BMWBaseEntity, SensorEntity):
|
||||
_LOGGER.debug(
|
||||
"Updating sensor '%s' of %s", self.entity_description.key, self.vehicle.name
|
||||
)
|
||||
if self.entity_description.key_class is None:
|
||||
state = getattr(self.vehicle, self.entity_description.key)
|
||||
else:
|
||||
state = getattr(
|
||||
getattr(self.vehicle, self.entity_description.key_class),
|
||||
self.entity_description.key,
|
||||
)
|
||||
|
||||
key_path = self.entity_description.key.split(".")
|
||||
state = getattr(self.vehicle, key_path.pop(0))
|
||||
|
||||
for key in key_path:
|
||||
state = getattr(state, key)
|
||||
|
||||
# For datetime without tzinfo, we assume it to be the same timezone as the HA instance
|
||||
if isinstance(state, datetime.datetime) and state.tzinfo is None:
|
||||
|
||||
@@ -149,6 +149,30 @@
|
||||
"inactive": "Inactive",
|
||||
"standby": "Standby"
|
||||
}
|
||||
},
|
||||
"front_left_current_pressure": {
|
||||
"name": "Front left tire pressure"
|
||||
},
|
||||
"front_right_current_pressure": {
|
||||
"name": "Front right tire pressure"
|
||||
},
|
||||
"rear_left_current_pressure": {
|
||||
"name": "Rear left tire pressure"
|
||||
},
|
||||
"rear_right_current_pressure": {
|
||||
"name": "Rear right tire pressure"
|
||||
},
|
||||
"front_left_target_pressure": {
|
||||
"name": "Front left target pressure"
|
||||
},
|
||||
"front_right_target_pressure": {
|
||||
"name": "Front right target pressure"
|
||||
},
|
||||
"rear_left_target_pressure": {
|
||||
"name": "Rear left target pressure"
|
||||
},
|
||||
"rear_right_target_pressure": {
|
||||
"name": "Rear right target pressure"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
@@ -159,5 +183,19 @@
|
||||
"name": "Charging"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"regions": {
|
||||
"options": {
|
||||
"china": "China",
|
||||
"north_america": "North America",
|
||||
"rest_of_world": "Rest of world"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_poi": {
|
||||
"message": "Invalid data for point of interest: {poi_exception}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,13 @@ from bimmer_connected.vehicle import MyBMWVehicle
|
||||
from bimmer_connected.vehicle.fuel_and_battery import ChargingState
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import BMWBaseEntity
|
||||
from .const import DOMAIN
|
||||
from . import BMWConfigEntry
|
||||
from .coordinator import BMWDataUpdateCoordinator
|
||||
from .entity import BMWBaseEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,11 +63,11 @@ NUMBER_TYPES: list[BMWSwitchEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BMWConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the MyBMW switch from config entry."""
|
||||
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
entities: list[BMWSwitch] = []
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bond_async import Action, BPUPSubscriptions
|
||||
from bond_async import Action
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -12,7 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import BondConfigEntry
|
||||
from .entity import BondEntity
|
||||
from .utils import BondDevice, BondHub
|
||||
from .models import BondData
|
||||
from .utils import BondDevice
|
||||
|
||||
# The api requires a step size even though it does not
|
||||
# seem to matter what is is as the underlying device is likely
|
||||
@@ -246,13 +247,11 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Bond button devices."""
|
||||
data = entry.runtime_data
|
||||
hub = data.hub
|
||||
bpup_subs = data.bpup_subs
|
||||
entities: list[BondButtonEntity] = []
|
||||
|
||||
for device in hub.devices:
|
||||
for device in data.hub.devices:
|
||||
device_entities = [
|
||||
BondButtonEntity(hub, device, bpup_subs, description)
|
||||
BondButtonEntity(data, device, description)
|
||||
for description in BUTTONS
|
||||
if device.has_action(description.key)
|
||||
and (
|
||||
@@ -264,9 +263,7 @@ async def async_setup_entry(
|
||||
# Most devices have the stop action available, but
|
||||
# we only add the stop action button if we add actions
|
||||
# since its not so useful if there are no actions to stop
|
||||
device_entities.append(
|
||||
BondButtonEntity(hub, device, bpup_subs, STOP_BUTTON)
|
||||
)
|
||||
device_entities.append(BondButtonEntity(data, device, STOP_BUTTON))
|
||||
entities.extend(device_entities)
|
||||
|
||||
async_add_entities(entities)
|
||||
@@ -279,26 +276,23 @@ class BondButtonEntity(BondEntity, ButtonEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hub: BondHub,
|
||||
data: BondData,
|
||||
device: BondDevice,
|
||||
bpup_subs: BPUPSubscriptions,
|
||||
description: BondButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Init Bond button."""
|
||||
self.entity_description = description
|
||||
super().__init__(
|
||||
hub, device, bpup_subs, description.name, description.key.lower()
|
||||
)
|
||||
super().__init__(data, device, description.name, description.key.lower())
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
if self.entity_description.argument:
|
||||
action = Action(
|
||||
self.entity_description.key, self.entity_description.argument
|
||||
)
|
||||
description = self.entity_description
|
||||
key = description.key
|
||||
if argument := description.argument:
|
||||
action = Action(key, argument)
|
||||
else:
|
||||
action = Action(self.entity_description.key)
|
||||
await self._hub.bond.action(self._device.device_id, action)
|
||||
action = Action(key)
|
||||
await self._bond.action(self._device_id, action)
|
||||
|
||||
def _apply_state(self) -> None:
|
||||
"""Apply the state."""
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from bond_async import Action, BPUPSubscriptions, DeviceType
|
||||
from bond_async import Action, DeviceType
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
@@ -17,7 +17,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import BondConfigEntry
|
||||
from .entity import BondEntity
|
||||
from .utils import BondDevice, BondHub
|
||||
from .models import BondData
|
||||
from .utils import BondDevice
|
||||
|
||||
|
||||
def _bond_to_hass_position(bond_position: int) -> int:
|
||||
@@ -37,12 +38,9 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Bond cover devices."""
|
||||
data = entry.runtime_data
|
||||
hub = data.hub
|
||||
bpup_subs = data.bpup_subs
|
||||
|
||||
async_add_entities(
|
||||
BondCover(hub, device, bpup_subs)
|
||||
for device in hub.devices
|
||||
BondCover(data, device)
|
||||
for device in data.hub.devices
|
||||
if device.type == DeviceType.MOTORIZED_SHADES
|
||||
)
|
||||
|
||||
@@ -52,11 +50,9 @@ class BondCover(BondEntity, CoverEntity):
|
||||
|
||||
_attr_device_class = CoverDeviceClass.SHADE
|
||||
|
||||
def __init__(
|
||||
self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions
|
||||
) -> None:
|
||||
def __init__(self, data: BondData, device: BondDevice) -> None:
|
||||
"""Create HA entity representing Bond cover."""
|
||||
super().__init__(hub, device, bpup_subs)
|
||||
super().__init__(data, device)
|
||||
supported_features = CoverEntityFeature(0)
|
||||
if self._device.supports_set_position():
|
||||
supported_features |= CoverEntityFeature.SET_POSITION
|
||||
@@ -84,31 +80,31 @@ class BondCover(BondEntity, CoverEntity):
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Set the cover position."""
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id,
|
||||
await self._bond.action(
|
||||
self._device_id,
|
||||
Action.set_position(_hass_to_bond_position(kwargs[ATTR_POSITION])),
|
||||
)
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
await self._hub.bond.action(self._device.device_id, Action.open())
|
||||
await self._bond.action(self._device_id, Action.open())
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close cover."""
|
||||
await self._hub.bond.action(self._device.device_id, Action.close())
|
||||
await self._bond.action(self._device_id, Action.close())
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Hold cover."""
|
||||
await self._hub.bond.action(self._device.device_id, Action.hold())
|
||||
await self._bond.action(self._device_id, Action.hold())
|
||||
|
||||
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Open the cover tilt."""
|
||||
await self._hub.bond.action(self._device.device_id, Action.tilt_open())
|
||||
await self._bond.action(self._device_id, Action.tilt_open())
|
||||
|
||||
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Close the cover tilt."""
|
||||
await self._hub.bond.action(self._device.device_id, Action.tilt_close())
|
||||
await self._bond.action(self._device_id, Action.tilt_close())
|
||||
|
||||
async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
await self._hub.bond.action(self._device.device_id, Action.hold())
|
||||
await self._bond.action(self._device_id, Action.hold())
|
||||
|
||||
@@ -8,7 +8,6 @@ from datetime import datetime
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientError
|
||||
from bond_async import BPUPSubscriptions
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_HW_VERSION,
|
||||
@@ -24,7 +23,8 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .const import DOMAIN
|
||||
from .utils import BondDevice, BondHub
|
||||
from .models import BondData
|
||||
from .utils import BondDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -39,19 +39,20 @@ class BondEntity(Entity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hub: BondHub,
|
||||
data: BondData,
|
||||
device: BondDevice,
|
||||
bpup_subs: BPUPSubscriptions,
|
||||
sub_device: str | None = None,
|
||||
sub_device_id: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize entity with API and device info."""
|
||||
hub = data.hub
|
||||
self._hub = hub
|
||||
self._bond = hub.bond
|
||||
self._device = device
|
||||
self._device_id = device.device_id
|
||||
self._sub_device = sub_device
|
||||
self._attr_available = True
|
||||
self._bpup_subs = bpup_subs
|
||||
self._bpup_subs = data.bpup_subs
|
||||
self._update_lock = Lock()
|
||||
self._initialized = False
|
||||
if sub_device_id:
|
||||
@@ -79,7 +80,7 @@ class BondEntity(Entity):
|
||||
device_info = DeviceInfo(
|
||||
manufacturer=self._hub.make,
|
||||
# type ignore: tuple items should not be Optional
|
||||
identifiers={(DOMAIN, self._hub.bond_id, self._device.device_id)}, # type: ignore[arg-type]
|
||||
identifiers={(DOMAIN, self._hub.bond_id, self._device_id)}, # type: ignore[arg-type]
|
||||
configuration_url=f"http://{self._hub.host}",
|
||||
)
|
||||
if self.name is not None:
|
||||
@@ -141,7 +142,7 @@ class BondEntity(Entity):
|
||||
async def _async_update_from_api(self) -> None:
|
||||
"""Fetch via the API."""
|
||||
try:
|
||||
state: dict = await self._hub.bond.device_state(self._device_id)
|
||||
state: dict = await self._bond.device_state(self._device_id)
|
||||
except (ClientError, TimeoutError, OSError) as error:
|
||||
if self.available:
|
||||
_LOGGER.warning(
|
||||
|
||||
@@ -7,7 +7,7 @@ import math
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
from bond_async import Action, BPUPSubscriptions, DeviceType, Direction
|
||||
from bond_async import Action, DeviceType, Direction
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
@@ -29,7 +29,8 @@ from homeassistant.util.scaling import int_states_in_range
|
||||
from . import BondConfigEntry
|
||||
from .const import SERVICE_SET_FAN_SPEED_TRACKED_STATE
|
||||
from .entity import BondEntity
|
||||
from .utils import BondDevice, BondHub
|
||||
from .models import BondData
|
||||
from .utils import BondDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,8 +44,6 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Bond fan devices."""
|
||||
data = entry.runtime_data
|
||||
hub = data.hub
|
||||
bpup_subs = data.bpup_subs
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_FAN_SPEED_TRACKED_STATE,
|
||||
@@ -53,8 +52,8 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
BondFan(hub, device, bpup_subs)
|
||||
for device in hub.devices
|
||||
BondFan(data, device)
|
||||
for device in data.hub.devices
|
||||
if DeviceType.is_fan(device.type)
|
||||
)
|
||||
|
||||
@@ -62,14 +61,12 @@ async def async_setup_entry(
|
||||
class BondFan(BondEntity, FanEntity):
|
||||
"""Representation of a Bond fan."""
|
||||
|
||||
def __init__(
|
||||
self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions
|
||||
) -> None:
|
||||
def __init__(self, data: BondData, device: BondDevice) -> None:
|
||||
"""Create HA entity representing Bond fan."""
|
||||
self._power: bool | None = None
|
||||
self._speed: int | None = None
|
||||
self._direction: int | None = None
|
||||
super().__init__(hub, device, bpup_subs)
|
||||
super().__init__(data, device)
|
||||
if self._device.has_action(Action.BREEZE_ON):
|
||||
self._attr_preset_modes = [PRESET_MODE_BREEZE]
|
||||
features = FanEntityFeature(0)
|
||||
@@ -136,15 +133,13 @@ class BondFan(BondEntity, FanEntity):
|
||||
bond_speed,
|
||||
)
|
||||
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action.set_speed(bond_speed)
|
||||
)
|
||||
await self._bond.action(self._device_id, Action.set_speed(bond_speed))
|
||||
|
||||
async def async_set_power_belief(self, power_state: bool) -> None:
|
||||
"""Set the believed state to on or off."""
|
||||
try:
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action.set_power_state_belief(power_state)
|
||||
await self._bond.action(
|
||||
self._device_id, Action.set_power_state_belief(power_state)
|
||||
)
|
||||
except ClientResponseError as ex:
|
||||
raise HomeAssistantError(
|
||||
@@ -168,8 +163,8 @@ class BondFan(BondEntity, FanEntity):
|
||||
bond_speed,
|
||||
)
|
||||
try:
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action.set_speed_belief(bond_speed)
|
||||
await self._bond.action(
|
||||
self._device_id, Action.set_speed_belief(bond_speed)
|
||||
)
|
||||
except ClientResponseError as ex:
|
||||
raise HomeAssistantError(
|
||||
@@ -191,25 +186,21 @@ class BondFan(BondEntity, FanEntity):
|
||||
elif percentage is not None:
|
||||
await self.async_set_percentage(percentage)
|
||||
else:
|
||||
await self._hub.bond.action(self._device.device_id, Action.turn_on())
|
||||
await self._bond.action(self._device_id, Action.turn_on())
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
await self._hub.bond.action(self._device.device_id, Action(Action.BREEZE_ON))
|
||||
await self._bond.action(self._device_id, Action(Action.BREEZE_ON))
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the fan off."""
|
||||
if self.preset_mode == PRESET_MODE_BREEZE:
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action(Action.BREEZE_OFF)
|
||||
)
|
||||
await self._hub.bond.action(self._device.device_id, Action.turn_off())
|
||||
await self._bond.action(self._device_id, Action(Action.BREEZE_OFF))
|
||||
await self._bond.action(self._device_id, Action.turn_off())
|
||||
|
||||
async def async_set_direction(self, direction: str) -> None:
|
||||
"""Set fan rotation direction."""
|
||||
bond_direction = (
|
||||
Direction.REVERSE if direction == DIRECTION_REVERSE else Direction.FORWARD
|
||||
)
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action.set_direction(bond_direction)
|
||||
)
|
||||
await self._bond.action(self._device_id, Action.set_direction(bond_direction))
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
from bond_async import Action, BPUPSubscriptions, DeviceType
|
||||
from bond_async import Action, DeviceType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
@@ -23,7 +23,8 @@ from .const import (
|
||||
SERVICE_SET_LIGHT_POWER_TRACKED_STATE,
|
||||
)
|
||||
from .entity import BondEntity
|
||||
from .utils import BondDevice, BondHub
|
||||
from .models import BondData
|
||||
from .utils import BondDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -46,8 +47,6 @@ async def async_setup_entry(
|
||||
"""Set up Bond light devices."""
|
||||
data = entry.runtime_data
|
||||
hub = data.hub
|
||||
bpup_subs = data.bpup_subs
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
for service in ENTITY_SERVICES:
|
||||
@@ -58,7 +57,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
fan_lights: list[Entity] = [
|
||||
BondLight(hub, device, bpup_subs)
|
||||
BondLight(data, device)
|
||||
for device in hub.devices
|
||||
if DeviceType.is_fan(device.type)
|
||||
and device.supports_light()
|
||||
@@ -66,31 +65,31 @@ async def async_setup_entry(
|
||||
]
|
||||
|
||||
fan_up_lights: list[Entity] = [
|
||||
BondUpLight(hub, device, bpup_subs, "up_light")
|
||||
BondUpLight(data, device, "up_light")
|
||||
for device in hub.devices
|
||||
if DeviceType.is_fan(device.type) and device.supports_up_light()
|
||||
]
|
||||
|
||||
fan_down_lights: list[Entity] = [
|
||||
BondDownLight(hub, device, bpup_subs, "down_light")
|
||||
BondDownLight(data, device, "down_light")
|
||||
for device in hub.devices
|
||||
if DeviceType.is_fan(device.type) and device.supports_down_light()
|
||||
]
|
||||
|
||||
fireplaces: list[Entity] = [
|
||||
BondFireplace(hub, device, bpup_subs)
|
||||
BondFireplace(data, device)
|
||||
for device in hub.devices
|
||||
if DeviceType.is_fireplace(device.type)
|
||||
]
|
||||
|
||||
fp_lights: list[Entity] = [
|
||||
BondLight(hub, device, bpup_subs, "light")
|
||||
BondLight(data, device, "light")
|
||||
for device in hub.devices
|
||||
if DeviceType.is_fireplace(device.type) and device.supports_light()
|
||||
]
|
||||
|
||||
lights: list[Entity] = [
|
||||
BondLight(hub, device, bpup_subs)
|
||||
BondLight(data, device)
|
||||
for device in hub.devices
|
||||
if DeviceType.is_light(device.type)
|
||||
]
|
||||
@@ -130,8 +129,8 @@ class BondBaseLight(BondEntity, LightEntity):
|
||||
await self.async_set_power_belief(False)
|
||||
return
|
||||
try:
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id,
|
||||
await self._bond.action(
|
||||
self._device_id,
|
||||
Action.set_brightness_belief(round((brightness * 100) / 255)),
|
||||
)
|
||||
except ClientResponseError as ex:
|
||||
@@ -143,8 +142,8 @@ class BondBaseLight(BondEntity, LightEntity):
|
||||
async def async_set_power_belief(self, power_state: bool) -> None:
|
||||
"""Set the belief state of the light."""
|
||||
try:
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action.set_light_state_belief(power_state)
|
||||
await self._bond.action(
|
||||
self._device_id, Action.set_light_state_belief(power_state)
|
||||
)
|
||||
except ClientResponseError as ex:
|
||||
raise HomeAssistantError(
|
||||
@@ -158,13 +157,12 @@ class BondLight(BondBaseLight, BondEntity, LightEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hub: BondHub,
|
||||
data: BondData,
|
||||
device: BondDevice,
|
||||
bpup_subs: BPUPSubscriptions,
|
||||
sub_device: str | None = None,
|
||||
) -> None:
|
||||
"""Create HA entity representing Bond light."""
|
||||
super().__init__(hub, device, bpup_subs, sub_device)
|
||||
super().__init__(data, device, sub_device)
|
||||
if device.supports_set_brightness():
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
@@ -178,16 +176,16 @@ class BondLight(BondBaseLight, BondEntity, LightEntity):
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
if brightness := kwargs.get(ATTR_BRIGHTNESS):
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id,
|
||||
await self._bond.action(
|
||||
self._device_id,
|
||||
Action.set_brightness(round((brightness * 100) / 255)),
|
||||
)
|
||||
else:
|
||||
await self._hub.bond.action(self._device.device_id, Action.turn_light_on())
|
||||
await self._bond.action(self._device_id, Action.turn_light_on())
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
await self._hub.bond.action(self._device.device_id, Action.turn_light_off())
|
||||
await self._bond.action(self._device_id, Action.turn_light_off())
|
||||
|
||||
@callback
|
||||
def _async_has_action_or_raise(self, action: str) -> None:
|
||||
@@ -202,8 +200,8 @@ class BondLight(BondBaseLight, BondEntity, LightEntity):
|
||||
" replaced with a button; Call the button.press service instead"
|
||||
)
|
||||
self._async_has_action_or_raise(Action.START_INCREASING_BRIGHTNESS)
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action(Action.START_INCREASING_BRIGHTNESS)
|
||||
await self._bond.action(
|
||||
self._device_id, Action(Action.START_INCREASING_BRIGHTNESS)
|
||||
)
|
||||
|
||||
async def async_start_decreasing_brightness(self) -> None:
|
||||
@@ -213,8 +211,8 @@ class BondLight(BondBaseLight, BondEntity, LightEntity):
|
||||
" replaced with a button; Call the button.press service instead"
|
||||
)
|
||||
self._async_has_action_or_raise(Action.START_DECREASING_BRIGHTNESS)
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action(Action.START_DECREASING_BRIGHTNESS)
|
||||
await self._bond.action(
|
||||
self._device_id, Action(Action.START_DECREASING_BRIGHTNESS)
|
||||
)
|
||||
|
||||
async def async_stop(self) -> None:
|
||||
@@ -224,7 +222,7 @@ class BondLight(BondBaseLight, BondEntity, LightEntity):
|
||||
" Call the button.press service instead"
|
||||
)
|
||||
self._async_has_action_or_raise(Action.STOP)
|
||||
await self._hub.bond.action(self._device.device_id, Action(Action.STOP))
|
||||
await self._bond.action(self._device_id, Action(Action.STOP))
|
||||
|
||||
|
||||
class BondDownLight(BondBaseLight, BondEntity, LightEntity):
|
||||
@@ -236,15 +234,11 @@ class BondDownLight(BondBaseLight, BondEntity, LightEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action(Action.TURN_DOWN_LIGHT_ON)
|
||||
)
|
||||
await self._bond.action(self._device_id, Action(Action.TURN_DOWN_LIGHT_ON))
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action(Action.TURN_DOWN_LIGHT_OFF)
|
||||
)
|
||||
await self._bond.action(self._device_id, Action(Action.TURN_DOWN_LIGHT_OFF))
|
||||
|
||||
|
||||
class BondUpLight(BondBaseLight, BondEntity, LightEntity):
|
||||
@@ -256,15 +250,11 @@ class BondUpLight(BondBaseLight, BondEntity, LightEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action(Action.TURN_UP_LIGHT_ON)
|
||||
)
|
||||
await self._bond.action(self._device_id, Action(Action.TURN_UP_LIGHT_ON))
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action(Action.TURN_UP_LIGHT_OFF)
|
||||
)
|
||||
await self._bond.action(self._device_id, Action(Action.TURN_UP_LIGHT_OFF))
|
||||
|
||||
|
||||
class BondFireplace(BondEntity, LightEntity):
|
||||
@@ -287,15 +277,15 @@ class BondFireplace(BondEntity, LightEntity):
|
||||
|
||||
if brightness := kwargs.get(ATTR_BRIGHTNESS):
|
||||
flame = round((brightness * 100) / 255)
|
||||
await self._hub.bond.action(self._device.device_id, Action.set_flame(flame))
|
||||
await self._bond.action(self._device_id, Action.set_flame(flame))
|
||||
else:
|
||||
await self._hub.bond.action(self._device.device_id, Action.turn_on())
|
||||
await self._bond.action(self._device_id, Action.turn_on())
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the fireplace off."""
|
||||
_LOGGER.debug("Fireplace async_turn_off called with: %s", kwargs)
|
||||
|
||||
await self._hub.bond.action(self._device.device_id, Action.turn_off())
|
||||
await self._bond.action(self._device_id, Action.turn_off())
|
||||
|
||||
async def async_set_brightness_belief(self, brightness: int) -> None:
|
||||
"""Set the belief state of the light."""
|
||||
@@ -305,8 +295,8 @@ class BondFireplace(BondEntity, LightEntity):
|
||||
await self.async_set_power_belief(False)
|
||||
return
|
||||
try:
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id,
|
||||
await self._bond.action(
|
||||
self._device_id,
|
||||
Action.set_brightness_belief(round((brightness * 100) / 255)),
|
||||
)
|
||||
except ClientResponseError as ex:
|
||||
@@ -318,8 +308,8 @@ class BondFireplace(BondEntity, LightEntity):
|
||||
async def async_set_power_belief(self, power_state: bool) -> None:
|
||||
"""Set the belief state of the light."""
|
||||
try:
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action.set_power_state_belief(power_state)
|
||||
await self._bond.action(
|
||||
self._device_id, Action.set_power_state_belief(power_state)
|
||||
)
|
||||
except ClientResponseError as ex:
|
||||
raise HomeAssistantError(
|
||||
|
||||
@@ -26,8 +26,6 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Bond generic devices."""
|
||||
data = entry.runtime_data
|
||||
hub = data.hub
|
||||
bpup_subs = data.bpup_subs
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_POWER_TRACKED_STATE,
|
||||
@@ -36,8 +34,8 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
BondSwitch(hub, device, bpup_subs)
|
||||
for device in hub.devices
|
||||
BondSwitch(data, device)
|
||||
for device in data.hub.devices
|
||||
if DeviceType.is_generic(device.type)
|
||||
)
|
||||
|
||||
@@ -50,17 +48,17 @@ class BondSwitch(BondEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
await self._hub.bond.action(self._device.device_id, Action.turn_on())
|
||||
await self._bond.action(self._device_id, Action.turn_on())
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
await self._hub.bond.action(self._device.device_id, Action.turn_off())
|
||||
await self._bond.action(self._device_id, Action.turn_off())
|
||||
|
||||
async def async_set_power_belief(self, power_state: bool) -> None:
|
||||
"""Set switch power belief."""
|
||||
try:
|
||||
await self._hub.bond.action(
|
||||
self._device.device_id, Action.set_power_state_belief(power_state)
|
||||
await self._bond.action(
|
||||
self._device_id, Action.set_power_state_belief(power_state)
|
||||
)
|
||||
except ClientResponseError as ex:
|
||||
raise HomeAssistantError(
|
||||
|
||||
@@ -6,6 +6,7 @@ DOMAIN = "broadlink"
|
||||
|
||||
DOMAINS_AND_TYPES = {
|
||||
Platform.CLIMATE: {"HYS"},
|
||||
Platform.LIGHT: {"LB1", "LB2"},
|
||||
Platform.REMOTE: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"},
|
||||
Platform.SENSOR: {
|
||||
"A1",
|
||||
@@ -35,7 +36,7 @@ DOMAINS_AND_TYPES = {
|
||||
"SP4",
|
||||
"SP4B",
|
||||
},
|
||||
Platform.LIGHT: {"LB1", "LB2"},
|
||||
Platform.TIME: {"HYS"},
|
||||
}
|
||||
DEVICE_TYPES = set.union(*DOMAINS_AND_TYPES.values())
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Generic
|
||||
|
||||
import broadlink as blk
|
||||
from broadlink.exceptions import (
|
||||
@@ -12,6 +13,7 @@ from broadlink.exceptions import (
|
||||
ConnectionClosedError,
|
||||
NetworkTimeoutError,
|
||||
)
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@@ -27,7 +29,9 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import DEFAULT_PORT, DOMAIN, DOMAINS_AND_TYPES
|
||||
from .updater import get_update_manager
|
||||
from .updater import BroadlinkUpdateManager, get_update_manager
|
||||
|
||||
_ApiT = TypeVar("_ApiT", bound=blk.Device, default=blk.Device)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,16 +41,16 @@ def get_domains(device_type: str) -> set[Platform]:
|
||||
return {d for d, t in DOMAINS_AND_TYPES.items() if device_type in t}
|
||||
|
||||
|
||||
class BroadlinkDevice:
|
||||
class BroadlinkDevice(Generic[_ApiT]):
|
||||
"""Manages a Broadlink device."""
|
||||
|
||||
api: blk.Device
|
||||
api: _ApiT
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None:
|
||||
"""Initialize the device."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
self.update_manager = None
|
||||
self.update_manager: BroadlinkUpdateManager[_ApiT] | None = None
|
||||
self.fw_version: int | None = None
|
||||
self.authorized: bool | None = None
|
||||
self.reset_jobs: list[CALLBACK_TYPE] = []
|
||||
|
||||
@@ -20,6 +20,8 @@ class BroadlinkEntity(Entity):
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when the entity is added to hass."""
|
||||
self.async_on_remove(self._coordinator.async_add_listener(self._recv_data))
|
||||
if self._coordinator.data:
|
||||
self._update_state(self._coordinator.data)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the state of the entity."""
|
||||
|
||||
63
homeassistant/components/broadlink/time.py
Normal file
63
homeassistant/components/broadlink/time.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Support for Broadlink device time."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import time
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.time import TimeEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import BroadlinkDevice
|
||||
from .const import DOMAIN
|
||||
from .entity import BroadlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Broadlink time."""
|
||||
device = hass.data[DOMAIN].devices[config_entry.entry_id]
|
||||
async_add_entities([BroadlinkTime(device)])
|
||||
|
||||
|
||||
class BroadlinkTime(BroadlinkEntity, TimeEntity):
|
||||
"""Representation of a Broadlink device time."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_native_value: time | None = None
|
||||
|
||||
def __init__(self, device: BroadlinkDevice) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(device)
|
||||
|
||||
self._attr_unique_id = f"{device.unique_id}-device_time"
|
||||
|
||||
def _update_state(self, data: dict[str, Any]) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if data is None or "hour" not in data or "min" not in data or "sec" not in data:
|
||||
self._attr_native_value = None
|
||||
else:
|
||||
self._attr_native_value = time(
|
||||
hour=data["hour"],
|
||||
minute=data["min"],
|
||||
second=data["sec"],
|
||||
tzinfo=dt_util.get_default_time_zone(),
|
||||
)
|
||||
|
||||
async def async_set_value(self, value: time) -> None:
|
||||
"""Change the value."""
|
||||
await self._device.async_request(
|
||||
self._device.api.set_time,
|
||||
hour=value.hour,
|
||||
minute=value.minute,
|
||||
second=value.second,
|
||||
day=self._coordinator.data["dayofweek"],
|
||||
)
|
||||
self._attr_native_value = value
|
||||
self.async_write_ha_state()
|
||||
@@ -1,20 +1,30 @@
|
||||
"""Support for fetching data from Broadlink devices."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Generic
|
||||
|
||||
import broadlink as blk
|
||||
from broadlink.exceptions import AuthorizationError, BroadlinkException
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .device import BroadlinkDevice
|
||||
|
||||
_ApiT = TypeVar("_ApiT", bound=blk.Device)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_update_manager(device):
|
||||
def get_update_manager(device: BroadlinkDevice[_ApiT]) -> BroadlinkUpdateManager[_ApiT]:
|
||||
"""Return an update manager for a given Broadlink device."""
|
||||
update_managers = {
|
||||
update_managers: dict[str, type[BroadlinkUpdateManager]] = {
|
||||
"A1": BroadlinkA1UpdateManager,
|
||||
"BG1": BroadlinkBG1UpdateManager,
|
||||
"HYS": BroadlinkThermostatUpdateManager,
|
||||
@@ -38,7 +48,7 @@ def get_update_manager(device):
|
||||
return update_managers[device.api.type](device)
|
||||
|
||||
|
||||
class BroadlinkUpdateManager(ABC):
|
||||
class BroadlinkUpdateManager(ABC, Generic[_ApiT]):
|
||||
"""Representation of a Broadlink update manager.
|
||||
|
||||
Implement this class to manage fetching data from the device and to
|
||||
@@ -47,7 +57,7 @@ class BroadlinkUpdateManager(ABC):
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, device: BroadlinkDevice[_ApiT]) -> None:
|
||||
"""Initialize the update manager."""
|
||||
self.device = device
|
||||
self.coordinator = DataUpdateCoordinator(
|
||||
@@ -57,18 +67,22 @@ class BroadlinkUpdateManager(ABC):
|
||||
update_method=self.async_update,
|
||||
update_interval=self.SCAN_INTERVAL,
|
||||
)
|
||||
self.available = None
|
||||
self.last_update = None
|
||||
self.available: bool | None = None
|
||||
self.last_update: datetime | None = None
|
||||
|
||||
async def async_update(self):
|
||||
async def async_update(self) -> dict[str, Any] | None:
|
||||
"""Fetch data from the device and update availability."""
|
||||
try:
|
||||
data = await self.async_fetch_data()
|
||||
|
||||
except (BroadlinkException, OSError) as err:
|
||||
if self.available and (
|
||||
dt_util.utcnow() - self.last_update > self.SCAN_INTERVAL * 3
|
||||
or isinstance(err, (AuthorizationError, OSError))
|
||||
if (
|
||||
self.available
|
||||
and self.last_update
|
||||
and (
|
||||
dt_util.utcnow() - self.last_update > self.SCAN_INTERVAL * 3
|
||||
or isinstance(err, (AuthorizationError, OSError))
|
||||
)
|
||||
):
|
||||
self.available = False
|
||||
_LOGGER.warning(
|
||||
@@ -91,42 +105,42 @@ class BroadlinkUpdateManager(ABC):
|
||||
return data
|
||||
|
||||
@abstractmethod
|
||||
async def async_fetch_data(self):
|
||||
async def async_fetch_data(self) -> dict[str, Any] | None:
|
||||
"""Fetch data from the device."""
|
||||
|
||||
|
||||
class BroadlinkA1UpdateManager(BroadlinkUpdateManager):
|
||||
class BroadlinkA1UpdateManager(BroadlinkUpdateManager[blk.a1]):
|
||||
"""Manages updates for Broadlink A1 devices."""
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
async def async_fetch_data(self):
|
||||
async def async_fetch_data(self) -> dict[str, Any]:
|
||||
"""Fetch data from the device."""
|
||||
return await self.device.async_request(self.device.api.check_sensors_raw)
|
||||
|
||||
|
||||
class BroadlinkMP1UpdateManager(BroadlinkUpdateManager):
|
||||
class BroadlinkMP1UpdateManager(BroadlinkUpdateManager[blk.mp1]):
|
||||
"""Manages updates for Broadlink MP1 devices."""
|
||||
|
||||
async def async_fetch_data(self):
|
||||
async def async_fetch_data(self) -> dict[str, Any]:
|
||||
"""Fetch data from the device."""
|
||||
return await self.device.async_request(self.device.api.check_power)
|
||||
|
||||
|
||||
class BroadlinkMP1SUpdateManager(BroadlinkUpdateManager):
|
||||
class BroadlinkMP1SUpdateManager(BroadlinkUpdateManager[blk.mp1s]):
|
||||
"""Manages updates for Broadlink MP1 devices."""
|
||||
|
||||
async def async_fetch_data(self):
|
||||
async def async_fetch_data(self) -> dict[str, Any]:
|
||||
"""Fetch data from the device."""
|
||||
power = await self.device.async_request(self.device.api.check_power)
|
||||
sensors = await self.device.async_request(self.device.api.get_state)
|
||||
return {**power, **sensors}
|
||||
|
||||
|
||||
class BroadlinkRMUpdateManager(BroadlinkUpdateManager):
|
||||
class BroadlinkRMUpdateManager(BroadlinkUpdateManager[blk.rm]):
|
||||
"""Manages updates for Broadlink remotes."""
|
||||
|
||||
async def async_fetch_data(self):
|
||||
async def async_fetch_data(self) -> dict[str, Any]:
|
||||
"""Fetch data from the device."""
|
||||
device = self.device
|
||||
|
||||
@@ -138,7 +152,9 @@ class BroadlinkRMUpdateManager(BroadlinkUpdateManager):
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def normalize(data, previous_data):
|
||||
def normalize(
|
||||
data: dict[str, Any], previous_data: dict[str, Any] | None
|
||||
) -> dict[str, Any]:
|
||||
"""Fix firmware issue.
|
||||
|
||||
See https://github.com/home-assistant/core/issues/42100.
|
||||
@@ -151,18 +167,18 @@ class BroadlinkRMUpdateManager(BroadlinkUpdateManager):
|
||||
return data
|
||||
|
||||
|
||||
class BroadlinkSP1UpdateManager(BroadlinkUpdateManager):
|
||||
class BroadlinkSP1UpdateManager(BroadlinkUpdateManager[blk.sp1]):
|
||||
"""Manages updates for Broadlink SP1 devices."""
|
||||
|
||||
async def async_fetch_data(self):
|
||||
async def async_fetch_data(self) -> dict[str, Any] | None:
|
||||
"""Fetch data from the device."""
|
||||
return None
|
||||
|
||||
|
||||
class BroadlinkSP2UpdateManager(BroadlinkUpdateManager):
|
||||
class BroadlinkSP2UpdateManager(BroadlinkUpdateManager[blk.sp2]):
|
||||
"""Manages updates for Broadlink SP2 devices."""
|
||||
|
||||
async def async_fetch_data(self):
|
||||
async def async_fetch_data(self) -> dict[str, Any]:
|
||||
"""Fetch data from the device."""
|
||||
device = self.device
|
||||
|
||||
@@ -175,33 +191,33 @@ class BroadlinkSP2UpdateManager(BroadlinkUpdateManager):
|
||||
return data
|
||||
|
||||
|
||||
class BroadlinkBG1UpdateManager(BroadlinkUpdateManager):
|
||||
class BroadlinkBG1UpdateManager(BroadlinkUpdateManager[blk.bg1]):
|
||||
"""Manages updates for Broadlink BG1 devices."""
|
||||
|
||||
async def async_fetch_data(self):
|
||||
async def async_fetch_data(self) -> dict[str, Any]:
|
||||
"""Fetch data from the device."""
|
||||
return await self.device.async_request(self.device.api.get_state)
|
||||
|
||||
|
||||
class BroadlinkSP4UpdateManager(BroadlinkUpdateManager):
|
||||
class BroadlinkSP4UpdateManager(BroadlinkUpdateManager[blk.sp4]):
|
||||
"""Manages updates for Broadlink SP4 devices."""
|
||||
|
||||
async def async_fetch_data(self):
|
||||
async def async_fetch_data(self) -> dict[str, Any]:
|
||||
"""Fetch data from the device."""
|
||||
return await self.device.async_request(self.device.api.get_state)
|
||||
|
||||
|
||||
class BroadlinkLB1UpdateManager(BroadlinkUpdateManager):
|
||||
class BroadlinkLB1UpdateManager(BroadlinkUpdateManager[blk.lb1]):
|
||||
"""Manages updates for Broadlink LB1 devices."""
|
||||
|
||||
async def async_fetch_data(self):
|
||||
async def async_fetch_data(self) -> dict[str, Any]:
|
||||
"""Fetch data from the device."""
|
||||
return await self.device.async_request(self.device.api.get_state)
|
||||
|
||||
|
||||
class BroadlinkThermostatUpdateManager(BroadlinkUpdateManager):
|
||||
class BroadlinkThermostatUpdateManager(BroadlinkUpdateManager[blk.hysen]):
|
||||
"""Manages updates for thermostats with Broadlink DNA."""
|
||||
|
||||
async def async_fetch_data(self):
|
||||
async def async_fetch_data(self) -> dict[str, Any]:
|
||||
"""Fetch data from the device."""
|
||||
return await self.device.async_request(self.device.api.get_full_status)
|
||||
|
||||
80
homeassistant/components/chacon_dio/__init__.py
Normal file
80
homeassistant/components/chacon_dio/__init__.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""The chacon_dio integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from dio_chacon_wifi_api import DIOChaconAPIClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.COVER]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChaconDioData:
|
||||
"""Chacon Dio data class."""
|
||||
|
||||
client: DIOChaconAPIClient
|
||||
list_devices: list[dict[str, Any]]
|
||||
|
||||
|
||||
type ChaconDioConfigEntry = ConfigEntry[ChaconDioData]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ChaconDioConfigEntry) -> bool:
|
||||
"""Set up chacon_dio from a config entry."""
|
||||
|
||||
config = entry.data
|
||||
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
dio_chacon_id = entry.unique_id
|
||||
|
||||
_LOGGER.debug("Initializing Chacon Dio client %s, %s", username, dio_chacon_id)
|
||||
client = DIOChaconAPIClient(
|
||||
username,
|
||||
password,
|
||||
dio_chacon_id,
|
||||
)
|
||||
|
||||
found_devices = await client.search_all_devices(with_state=True)
|
||||
list_devices = list(found_devices.values())
|
||||
_LOGGER.debug("List of devices %s", list_devices)
|
||||
|
||||
entry.runtime_data = ChaconDioData(
|
||||
client=client,
|
||||
list_devices=list_devices,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# Disconnect the permanent websocket connection of home assistant on shutdown
|
||||
async def _async_disconnect_websocket(_: Event) -> None:
|
||||
await client.disconnect()
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
await entry.runtime_data.client.disconnect()
|
||||
|
||||
return unload_ok
|
||||
67
homeassistant/components/chacon_dio/config_flow.py
Normal file
67
homeassistant/components/chacon_dio/config_flow.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Config flow for chacon_dio integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from dio_chacon_wifi_api import DIOChaconAPIClient
|
||||
from dio_chacon_wifi_api.exceptions import DIOChaconAPIError, DIOChaconInvalidAuthError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ChaconDioConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for chacon_dio."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
client = DIOChaconAPIClient(
|
||||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
|
||||
)
|
||||
try:
|
||||
_user_id: str = await client.get_user_id()
|
||||
except DIOChaconAPIError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except DIOChaconInvalidAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
else:
|
||||
await self.async_set_unique_id(_user_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"Chacon DiO {user_input[CONF_USERNAME]}",
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
finally:
|
||||
await client.disconnect()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
5
homeassistant/components/chacon_dio/const.py
Normal file
5
homeassistant/components/chacon_dio/const.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Constants for the chacon_dio integration."""
|
||||
|
||||
DOMAIN = "chacon_dio"
|
||||
|
||||
MANUFACTURER = "Chacon"
|
||||
124
homeassistant/components/chacon_dio/cover.py
Normal file
124
homeassistant/components/chacon_dio/cover.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""Cover Platform for Chacon Dio REV-SHUTTER devices."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from dio_chacon_wifi_api.const import DeviceTypeEnum, ShutterMoveEnum
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ChaconDioConfigEntry
|
||||
from .entity import ChaconDioEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ChaconDioConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Chacon Dio cover devices."""
|
||||
data = config_entry.runtime_data
|
||||
client = data.client
|
||||
|
||||
async_add_entities(
|
||||
ChaconDioCover(client, device)
|
||||
for device in data.list_devices
|
||||
if device["type"] == DeviceTypeEnum.SHUTTER.value
|
||||
)
|
||||
|
||||
|
||||
class ChaconDioCover(ChaconDioEntity, CoverEntity):
|
||||
"""Object for controlling a Chacon Dio cover."""
|
||||
|
||||
_attr_device_class = CoverDeviceClass.SHUTTER
|
||||
_attr_name = None
|
||||
|
||||
_attr_supported_features = (
|
||||
CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE
|
||||
| CoverEntityFeature.STOP
|
||||
| CoverEntityFeature.SET_POSITION
|
||||
)
|
||||
|
||||
def _update_attr(self, data: dict[str, Any]) -> None:
|
||||
"""Recomputes the attributes values either at init or when the device state changes."""
|
||||
self._attr_available = data["connected"]
|
||||
self._attr_current_cover_position = data["openlevel"]
|
||||
self._attr_is_closing = data["movement"] == ShutterMoveEnum.DOWN.value
|
||||
self._attr_is_opening = data["movement"] == ShutterMoveEnum.UP.value
|
||||
self._attr_is_closed = self._attr_current_cover_position == 0
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover.
|
||||
|
||||
Closed status is effective after the server callback that triggers callback_device_state.
|
||||
"""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Close cover %s , %s, %s",
|
||||
self.target_id,
|
||||
self._attr_name,
|
||||
self.is_closed,
|
||||
)
|
||||
|
||||
# closes effectively only if cover is not already closing and not fully closed
|
||||
if not self._attr_is_closing and not self.is_closed:
|
||||
self._attr_is_closing = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
await self.client.move_shutter_direction(
|
||||
self.target_id, ShutterMoveEnum.DOWN
|
||||
)
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover.
|
||||
|
||||
Opened status is effective after the server callback that triggers callback_device_state.
|
||||
"""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Open cover %s , %s, %s",
|
||||
self.target_id,
|
||||
self._attr_name,
|
||||
self.current_cover_position,
|
||||
)
|
||||
|
||||
# opens effectively only if cover is not already opening and not fully opened
|
||||
if not self._attr_is_opening and self.current_cover_position != 100:
|
||||
self._attr_is_opening = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
await self.client.move_shutter_direction(self.target_id, ShutterMoveEnum.UP)
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
|
||||
_LOGGER.debug("Stop cover %s , %s", self.target_id, self._attr_name)
|
||||
|
||||
self._attr_is_opening = False
|
||||
self._attr_is_closing = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
await self.client.move_shutter_direction(self.target_id, ShutterMoveEnum.STOP)
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Set the cover open position in percentage.
|
||||
|
||||
Closing or opening status is effective after the server callback that triggers callback_device_state.
|
||||
"""
|
||||
position: int = kwargs[ATTR_POSITION]
|
||||
|
||||
_LOGGER.debug(
|
||||
"Set cover position %i, %s , %s", position, self.target_id, self._attr_name
|
||||
)
|
||||
|
||||
await self.client.move_shutter_percentage(self.target_id, position)
|
||||
53
homeassistant/components/chacon_dio/entity.py
Normal file
53
homeassistant/components/chacon_dio/entity.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Base entity for the Chacon Dio entity."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from dio_chacon_wifi_api import DIOChaconAPIClient
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChaconDioEntity(Entity):
|
||||
"""Implements a common class elements representing the Chacon Dio entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, client: DIOChaconAPIClient, device: dict[str, Any]) -> None:
|
||||
"""Initialize Chacon Dio entity."""
|
||||
|
||||
self.client = client
|
||||
|
||||
self.target_id: str = device["id"]
|
||||
self._attr_unique_id = self.target_id
|
||||
self._attr_device_info: DeviceInfo | None = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.target_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=device["name"],
|
||||
model=device["model"],
|
||||
)
|
||||
|
||||
self._update_attr(device)
|
||||
|
||||
def _update_attr(self, data: dict[str, Any]) -> None:
|
||||
"""Recomputes the attributes values."""
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register the callback for server side events."""
|
||||
await super().async_added_to_hass()
|
||||
self.client.set_callback_device_state_by_device(
|
||||
self.target_id, self.callback_device_state
|
||||
)
|
||||
|
||||
def callback_device_state(self, data: dict[str, Any]) -> None:
|
||||
"""Receive callback for device state notification pushed from the server."""
|
||||
|
||||
_LOGGER.debug("Data received from server %s", data)
|
||||
self._update_attr(data)
|
||||
self.async_write_ha_state()
|
||||
10
homeassistant/components/chacon_dio/manifest.json
Normal file
10
homeassistant/components/chacon_dio/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "chacon_dio",
|
||||
"name": "Chacon DiO",
|
||||
"codeowners": ["@cnico"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/chacon_dio",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["dio_chacon_api"],
|
||||
"requirements": ["dio-chacon-wifi-api==1.1.0"]
|
||||
}
|
||||
20
homeassistant/components/chacon_dio/strings.json
Normal file
20
homeassistant/components/chacon_dio/strings.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class CiscoDeviceScanner(DeviceScanner):
|
||||
self.success_init = self._update_info()
|
||||
_LOGGER.info("Initialized cisco_ios scanner")
|
||||
|
||||
def get_device_name(self, device):
|
||||
async def async_get_device_name(self, device: str) -> str | None:
|
||||
"""Get the firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user