Compare commits
1000 commits
gj-2024102
...
dev
Author | SHA1 | Date | |
---|---|---|---|
|
1ce8bfdaa4 | ||
|
cd12720085 | ||
|
c7ee7dc880 | ||
|
472414a8d6 | ||
|
0c44c632d4 | ||
|
61d0de3042 | ||
|
01332a542c | ||
|
3d84e35268 | ||
|
eea782bbfe | ||
|
a949d18c30 | ||
|
a748897bd2 | ||
|
3201142fd8 | ||
|
d0a58b68e8 | ||
|
93f79be2f4 | ||
|
46cfe6aa32 | ||
|
301043ec38 | ||
|
245fc246d8 | ||
|
58fd917cb7 | ||
|
2c1d1f5777 | ||
|
938b1eca22 | ||
|
2fda4c82de | ||
|
4200913d03 | ||
|
4aad614497 | ||
|
6a3b4a6a23 | ||
|
51c6ee97b1 | ||
|
4002bc3c25 | ||
|
c35ef6bda3 | ||
|
ed5560aec2 | ||
|
0a5a2de78e | ||
|
7fd337d67f | ||
|
5f68d405b2 | ||
|
093b16c723 | ||
|
ac4cb52dbb | ||
|
72b976f832 | ||
|
f6bc5f050e | ||
|
8300afc00d | ||
|
ab11b84678 | ||
|
b78453b85b | ||
|
b270e4556c | ||
|
e90893e2bc | ||
|
a06e7e31b9 | ||
|
2eaaadd736 | ||
|
0ac00ef092 | ||
|
3092297979 | ||
|
827875473b | ||
|
5cce369ce8 | ||
|
fdb773c921 | ||
|
8b505a2273 | ||
|
a9f468509b | ||
|
4ff8b8015c | ||
|
5c52e865a0 | ||
|
6bfc0cbb0c | ||
|
388473ecd7 | ||
|
285468d85f | ||
|
167025a18c | ||
|
ac0c75a598 | ||
|
cb9cc0f801 | ||
|
7758d8ba48 | ||
|
7045b776b6 | ||
|
22aed92461 | ||
|
60bf0f6b06 | ||
|
3eab72b2aa | ||
|
d1c3e1caa9 | ||
|
8b547551e2 | ||
|
f1ce7ee8ce | ||
|
e388e9f396 | ||
|
96c12fdd10 | ||
|
e97a5f927c | ||
|
313309a7e0 | ||
|
ebe62501d6 | ||
|
c54369fe93 | ||
|
c89bf6a9aa | ||
|
906bdda6fa | ||
|
f3708549f0 | ||
|
3f34ddd74f | ||
|
b19c44b4a5 | ||
|
0cc50bc7bc | ||
|
e56dec2c8e | ||
|
e797149a16 | ||
|
c96f1c87a6 | ||
|
388c5807ea | ||
|
41c6eeedca | ||
|
829632b0af | ||
|
5293fc73d8 | ||
|
870bf388e0 | ||
|
7a4dac1eb1 | ||
|
88480d154a | ||
|
5497c440d9 | ||
|
1e26cf13d6 | ||
|
0dd208a4b9 | ||
|
c3492bc0ed | ||
|
85bf8d1374 | ||
|
e040eb0ff2 | ||
|
d7f41ff8a9 | ||
|
de5437f61e | ||
|
c52a893e21 | ||
|
f7f1830b7e | ||
|
784ad20fb6 | ||
|
0468e7e7a3 | ||
|
88c227681d | ||
|
3a37ff13a6 | ||
|
73929e6791 | ||
|
980b0fa5e6 | ||
|
fbc4a87166 | ||
|
7f9ec2a79e | ||
|
d8b55d39e4 | ||
|
ee41725b53 | ||
|
ae1203336d | ||
|
f10063c9be | ||
|
1da4579a09 | ||
|
7fd9339ad8 | ||
|
de391fa98b | ||
|
70211ab78e | ||
|
a1a08f7755 | ||
|
433321136d | ||
|
0677bba5bd | ||
|
d0ad834d93 | ||
|
7d2d6a82b0 | ||
|
e8dc62411a | ||
|
7925007ab4 | ||
|
7515deddab | ||
|
e382f924e6 | ||
|
7fdcb98518 | ||
|
d0dbca41f7 | ||
|
f3229c723c | ||
|
cafa598fd6 | ||
|
73a62a09b0 | ||
|
ecd8dde347 | ||
|
31a2bb1b98 | ||
|
0fc019305e | ||
|
adb1c59859 | ||
|
5d0277a0d1 | ||
|
21d81d5a5c | ||
|
0de4bfcc2c | ||
|
2cc5486794 | ||
|
e3315383ab | ||
|
31b505828b | ||
|
b61580a937 | ||
|
928e5348e4 | ||
|
622682eb43 | ||
|
97fa568876 | ||
|
c10f078f2a | ||
|
e6d16f06fc | ||
|
c89ab7a142 | ||
|
6837ea947c | ||
|
5f0f29704b | ||
|
1f43dc6676 | ||
|
4d7405de2c | ||
|
4adffdd1a6 | ||
|
4e2f5bdb7d | ||
|
03bc711c51 | ||
|
8b8e949bdf | ||
|
69ba0d3a50 | ||
|
25fb70f281 | ||
|
0304588bb8 | ||
|
08f5081197 | ||
|
701f35488c | ||
|
d11012b2b7 | ||
|
8384100e1b | ||
|
cd0349ee4d | ||
|
b413e481cb | ||
|
9f7e6048f8 | ||
|
2802b77f21 | ||
|
964ad43a27 | ||
|
182be6e0ea | ||
|
cd11f01ace | ||
|
742eca5927 | ||
|
48e7fed901 | ||
|
0a4c0fe7cc | ||
|
9037cb8a7d | ||
|
c97cc34879 | ||
|
1ac9217630 | ||
|
e4036a2f14 | ||
|
da9c73a767 | ||
|
e4aaaf10c3 | ||
|
a7be76ba0a | ||
|
f7cc91903c | ||
|
4a8a674bd3 | ||
|
a8db25fbd8 | ||
|
2dc81ed866 | ||
|
c4762f3ff4 | ||
|
14285973b8 | ||
|
353ccf3ea7 | ||
|
6b90d8ff1a | ||
|
51e691f832 | ||
|
6c7ac7a6ef | ||
|
52ed1bf44a | ||
|
3eab0b704e | ||
|
1f32e02ba2 | ||
|
074418f8f7 | ||
|
b711b17193 | ||
|
03c3d09583 | ||
|
f49547d598 | ||
|
7678be8e2b | ||
|
7672215095 | ||
|
18cf96b92b | ||
|
94d597fd41 | ||
|
24b47b50ea | ||
|
e3dfa84d65 | ||
|
ed1366f463 | ||
|
5d5908a03f | ||
|
3062bad19e | ||
|
28832cbd3e | ||
|
ce94073321 | ||
|
fa61e02207 | ||
|
d1dab83f10 | ||
|
2b7d593ebe | ||
|
e407b4730d | ||
|
0d19e85a0d | ||
|
dac6271e01 | ||
|
8cae8edc55 | ||
|
a3b0909e3f | ||
|
ee30520b57 | ||
|
536e686892 | ||
|
ef767c2b9f | ||
|
c1ecc13cb3 | ||
|
c5e3ba536c | ||
|
0e324c074a | ||
|
a3ba7803db | ||
|
49bf5db5ff | ||
|
50981c26ad | ||
|
2adbf7c933 | ||
|
838ef0bb9f | ||
|
43c2658962 | ||
|
bbefa971d8 | ||
|
cb97f2f13c | ||
|
a657b9bb84 | ||
|
2d2f55a4df | ||
|
df16e6d022 | ||
|
56212c6fa5 | ||
|
bc964ce7f0 | ||
|
ed4f55406c | ||
|
03d5b18974 | ||
|
53c486ccd1 | ||
|
9a2a177b28 | ||
|
18e12740d9 | ||
|
5a24b670a2 | ||
|
94c5c8f42e | ||
|
e84d5fba11 | ||
|
b808c0c5eb | ||
|
782417528c | ||
|
7757423d18 | ||
|
e5a28f4f25 | ||
|
c18d50910f | ||
|
d4adb1f298 | ||
|
fe0a822721 | ||
|
9f427893b1 | ||
|
3b840c684b | ||
|
bc84fdc64a | ||
|
401262c23d | ||
|
795384ca2d | ||
|
dfc3423c83 | ||
|
22b5071c26 | ||
|
4b9524c5c1 | ||
|
9cd46c7f03 | ||
|
232a6868ff | ||
|
361e0d4fc7 | ||
|
26d8d5343a | ||
|
995aab8347 | ||
|
399011552b | ||
|
0c9f30364c | ||
|
bdc17621ee | ||
|
399c53a57e | ||
|
f55e13bde4 | ||
|
dea31e5744 | ||
|
48d9df89ac | ||
|
adf836d9ac | ||
|
51d6948848 | ||
|
7ce74cb5ec | ||
|
29ba140816 | ||
|
0ca4f3e1ba | ||
|
0430e6794e | ||
|
29fa7f827a | ||
|
57d1001603 | ||
|
96de4b3828 | ||
|
c6cb2884f4 | ||
|
27e81fe0ed | ||
|
2c1db10986 | ||
|
a7ba4bd086 | ||
|
25449b424f | ||
|
f6f89bd807 | ||
|
370d7d6bdf | ||
|
4dbf3359c1 | ||
|
25eb7173bf | ||
|
648c3d500b | ||
|
33016c2977 | ||
|
5679b061d2 | ||
|
2eb2bdd615 | ||
|
184cbfea23 | ||
|
f88bc008e5 | ||
|
a927312fb5 | ||
|
5f13db2356 | ||
|
64e84e2aa0 | ||
|
901457e7aa | ||
|
89a9c2ec24 | ||
|
9e04457472 | ||
|
6ecdbb677f | ||
|
211ce43127 | ||
|
f5555df990 | ||
|
82c2422990 | ||
|
734ebc1adb | ||
|
eb3371beef | ||
|
e1ef1063fe | ||
|
c355a53485 | ||
|
79de1d9ed4 | ||
|
7fefa5c235 | ||
|
94db78a0be | ||
|
83a1b06b56 | ||
|
1e42a38473 | ||
|
c54ed53a81 | ||
|
611a952232 | ||
|
05e76105ad | ||
|
ed56e5d631 | ||
|
9253fa4471 | ||
|
c85eb6bf8e | ||
|
cc30d34e87 | ||
|
14875a1101 | ||
|
030aebb97f | ||
|
6e2f36b6d4 | ||
|
25a05eb156 | ||
|
b71c4377f6 | ||
|
d671341864 | ||
|
383f712d43 | ||
|
8a20cd77a0 | ||
|
14023644ef | ||
|
496fc42b94 | ||
|
da0688ce8e | ||
|
89d3707cb7 | ||
|
3f5e395e2f | ||
|
00ea1cab9f | ||
|
5f36062ef3 | ||
|
e562b6f42b | ||
|
b76a94bd42 | ||
|
4e11ff05de | ||
|
080e3d7a42 | ||
|
69e3348cd7 | ||
|
6caa4baa00 | ||
|
4729b19dc6 | ||
|
8abbc4abbc | ||
|
3a667bce8c | ||
|
4c86102daf | ||
|
15bf652f37 | ||
|
eafed2b86c | ||
|
79901cede9 | ||
|
27dc82d7d0 | ||
|
ae37c8cc7a | ||
|
5eadfcc524 | ||
|
5fd1e23255 | ||
|
72bcc6702f | ||
|
8889464e04 | ||
|
af58b0c3b7 | ||
|
e9e20229a3 | ||
|
80ff6dc618 | ||
|
fa30100160 | ||
|
e6c20333b3 | ||
|
3858400a6f | ||
|
95eefbac20 | ||
|
e1e731eb48 | ||
|
f7ce4ff25c | ||
|
c7b2ffbc8e | ||
|
3a1502e2bb | ||
|
b830f83a34 | ||
|
2982e733bc | ||
|
e89ce215c6 | ||
|
b6345f8d07 | ||
|
9d261bab48 | ||
|
b6f875134e | ||
|
90ceebdf91 | ||
|
617e87e02c | ||
|
dafd54ba2b | ||
|
e8c3539709 | ||
|
e5263dc0c8 | ||
|
3584c710b9 | ||
|
0b56ef5699 | ||
|
90bd9bb626 | ||
|
03e6a13896 | ||
|
9fb3261f02 | ||
|
0bc6b8b0d4 | ||
|
18d2ced045 | ||
|
6c75e0bee1 | ||
|
0b981f42bb | ||
|
82868a8588 | ||
|
6e93777f54 | ||
|
9349292464 | ||
|
7084b3b52c | ||
|
0f0f5fd0ab | ||
|
cb0b942db3 | ||
|
b1c9f83952 | ||
|
1ff0efc97b | ||
|
a4da2a9eb5 | ||
|
ba3cfb5f87 | ||
|
bf196935f6 | ||
|
6e98343706 | ||
|
de453ab5c1 | ||
|
f408de4fc3 | ||
|
7863927c3a | ||
|
9fcf757021 | ||
|
fc0547ccdf | ||
|
22f8f117fb | ||
|
2052579efc | ||
|
b8f2583bc3 | ||
|
6323a078e1 | ||
|
ca0be3ec8a | ||
|
91157c21ef | ||
|
cc4fae10f5 | ||
|
d180ff417d | ||
|
8870b657d1 | ||
|
81735b7b47 | ||
|
7fd261347b | ||
|
df796d432e | ||
|
f6e36615d6 | ||
|
0278735dbf | ||
|
9c8d8fef16 | ||
|
6897b24c10 | ||
|
a2a3f59e65 | ||
|
2626a74840 | ||
|
689260f581 | ||
|
f1a2c8be4b | ||
|
0579d565dd | ||
|
f141f5f908 | ||
|
0c25252d9f | ||
|
400b377aa8 | ||
|
a5f3c434e0 | ||
|
365f8046ac | ||
|
4ac35d40cd | ||
|
7691991a93 | ||
|
d0c45b1857 | ||
|
02750452df | ||
|
41a81cbf15 | ||
|
ff621d5bf3 | ||
|
6d561a9796 | ||
|
4784199038 | ||
|
df35c8e707 | ||
|
57eeaf1f75 | ||
|
3cadc1796f | ||
|
ae06f734ce | ||
|
08a53362a7 | ||
|
274c928ec0 | ||
|
d75dda0c05 | ||
|
0c40fcdaeb | ||
|
0a1ba8a4a3 | ||
|
018acc0a3c | ||
|
3a293c6bc4 | ||
|
9155d56190 | ||
|
461dc13da9 | ||
|
b48e2127b8 | ||
|
11ab992dbb | ||
|
4be2cdf90a | ||
|
cdd5cb2876 | ||
|
cdc67aa891 | ||
|
6a22a2b867 | ||
|
0883b23d0c | ||
|
595459bfda | ||
|
5141a4d292 | ||
|
cf8b7607ae | ||
|
b38fe00387 | ||
|
5d446f0e14 | ||
|
a592ece9c8 | ||
|
9cb60c61d1 | ||
|
90ed06c354 | ||
|
22d64cb8f4 | ||
|
453039e860 | ||
|
e727162225 | ||
|
a898a5996e | ||
|
d501bb8d52 | ||
|
7ab8ff56b3 | ||
|
eda36512ec | ||
|
04aee812f8 | ||
|
6718cce203 | ||
|
49f0bb6990 | ||
|
38afcbb21f | ||
|
87ab2beddf | ||
|
a05a34239d | ||
|
f11aba9648 | ||
|
c2ef119e50 | ||
|
8b6c99776e | ||
|
463bffaeb6 | ||
|
0cfd8032c0 | ||
|
144d5ff0cc | ||
|
ab5c65b08c | ||
|
6b33bf3961 | ||
|
89eb395e2d | ||
|
d671d48869 | ||
|
ed582fae91 | ||
|
4d5c3ee0aa | ||
|
02046fcdb4 | ||
|
fbe27749a0 | ||
|
eddab96a69 | ||
|
ed3376352d | ||
|
dfbb763031 | ||
|
5cf13d9273 | ||
|
5ef45fd12e | ||
|
8a293a41f5 | ||
|
931820a170 | ||
|
e9944b964a | ||
|
dbae1d2f8b | ||
|
0dc8feba05 | ||
|
5c7c2347f7 | ||
|
d069907948 | ||
|
725ab477a8 | ||
|
d05ee9ff60 | ||
|
3c1f6d97cc | ||
|
5fe827f6c4 | ||
|
76f9a93ed7 | ||
|
df2506bfbb | ||
|
b25ab04d2c | ||
|
6f094e8a54 | ||
|
e18ffc53f2 | ||
|
0eea3176d6 | ||
|
4f20977a8e | ||
|
5bd63bb56b | ||
|
f7103da818 | ||
|
bf4922a7ef | ||
|
6f7eac5c6d | ||
|
d6e73a89f3 | ||
|
269aefd405 | ||
|
a6865f1639 | ||
|
f55aa0b86e | ||
|
02b34f05aa | ||
|
37f42707e5 | ||
|
17f3ba1434 | ||
|
31dcc25ba5 | ||
|
4da93f6a5e | ||
|
5ed7d32749 | ||
|
ab5b9dbdc9 | ||
|
3b28bf07d1 | ||
|
b626c9b450 | ||
|
5430eca93e | ||
|
b41c477f44 | ||
|
5900413c08 | ||
|
c2ceab741f | ||
|
45ff4940eb | ||
|
9c8a15cb64 | ||
|
b09e54c961 | ||
|
f44b7e202a | ||
|
0f535e979f | ||
|
4c2c01b4f6 | ||
|
b1d48fe9a2 | ||
|
41590f91ac | ||
|
e9d1f4f46e | ||
|
7f287412ba | ||
|
2df094de2b | ||
|
964ab5b351 | ||
|
3f6e9a54fe | ||
|
4ec5d5ae1e | ||
|
c49b155c29 | ||
|
fc602b1888 | ||
|
81421992a2 | ||
|
4ef31f9331 | ||
|
d7e304badf | ||
|
bf3f1b4b49 | ||
|
2ac0ff03fc | ||
|
d10553d624 | ||
|
b1dfc3cd23 | ||
|
696efe349e | ||
|
6a32722acc | ||
|
8eaec56c6b | ||
|
60d3c9342d | ||
|
4dc2433e8b | ||
|
2bd5039f28 | ||
|
8b1b14a704 | ||
|
5e674ce1d0 | ||
|
3656bcf752 | ||
|
39093fc2bc | ||
|
efa5838be4 | ||
|
1c6ad2fa66 | ||
|
af144e1b77 | ||
|
b451bfed81 | ||
|
3e32c50936 | ||
|
208b15637a | ||
|
c958cce769 | ||
|
602ec54579 | ||
|
fa2bfc5d9d | ||
|
94f906b34c | ||
|
60c93456c0 | ||
|
a4f210379d | ||
|
27e6205a37 | ||
|
3db6d82904 | ||
|
b8ddfd642e | ||
|
c98acd42db | ||
|
39f418f2d2 | ||
|
9fbd484dfe | ||
|
1773f2aadc | ||
|
cb1b72d6ba | ||
|
f5a2ec961d | ||
|
bf40e77d65 | ||
|
568bdef61f | ||
|
2303521778 | ||
|
3bf2946d13 | ||
|
484e5cb3e8 | ||
|
fbe8b6c34d | ||
|
4e7397dc9d | ||
|
a6189106e1 | ||
|
ed6123a3e6 | ||
|
0cd5deaa3f | ||
|
6c047e2678 | ||
|
405a480cae | ||
|
b4e69bab71 | ||
|
db81edfb2b | ||
|
24829bc44f | ||
|
c8594045df | ||
|
ea3f9b971f | ||
|
380974eed4 | ||
|
8151403bf6 | ||
|
16f5e76f00 | ||
|
b6b178cac0 | ||
|
0f020366e3 | ||
|
27a19be369 | ||
|
0c166eb307 | ||
|
79d73c28a7 | ||
|
2aed01b530 | ||
|
3fb0d61271 | ||
|
599acaf514 | ||
|
5f4103a4a7 | ||
|
c7c72231c7 | ||
|
6887a4419e | ||
|
db5cb6233c | ||
|
963829712d | ||
|
46ceccfbb3 | ||
|
aaf3039967 | ||
|
2509f18def | ||
|
a1e2d79613 | ||
|
96ba5c3983 | ||
|
041282190a | ||
|
8cdd5de75c | ||
|
a95c232f11 | ||
|
c9aba288b4 | ||
|
35a9d502af | ||
|
409c8783fe | ||
|
3adc3d7732 | ||
|
ec19712388 | ||
|
2c89e89c84 | ||
|
e602a464db | ||
|
ffc0651d89 | ||
|
7162efd836 | ||
|
8e7d782102 | ||
|
dc2028f99c | ||
|
f12ba5f7a9 | ||
|
45fb21e32d | ||
|
ecbb417736 | ||
|
3a59a862d5 | ||
|
e34fab0045 | ||
|
7254ebe0e3 | ||
|
b43bc3f32d | ||
|
ca3d13b5cc | ||
|
c8818bcce3 | ||
|
b234b5937a | ||
|
1bdef0f2f7 | ||
|
56fb61bd6f | ||
|
2c7d0b8909 | ||
|
cbb8d76da7 | ||
|
cce925c06c | ||
|
505a4bfc34 | ||
|
58e151966c | ||
|
8a6c9b7afc | ||
|
e72e2071b0 | ||
|
5d3af27928 | ||
|
5dc0bedbc4 | ||
|
8f7ae2665c | ||
|
10fdf819d3 | ||
|
02928601ef | ||
|
c227f6dc2c | ||
|
673f0224c9 | ||
|
79c602f59c | ||
|
07c070e253 | ||
|
9bda3bd477 | ||
|
2c9ad9562e | ||
|
c264ee22e7 | ||
|
f194a689cc | ||
|
a36b350954 | ||
|
db4278fb9d | ||
|
39ba4cff2f | ||
|
d68da74790 | ||
|
5fc45cd736 | ||
|
5ae2f3d081 | ||
|
478bf643bf | ||
|
7929895b11 | ||
|
da11a72b4c | ||
|
1649368cee | ||
|
a528d62c16 | ||
|
bd13dbdad0 | ||
|
8e7ffd9e16 | ||
|
f0bff09b5e | ||
|
0e959b3019 | ||
|
983cd9c3fc | ||
|
2236ca3e12 | ||
|
f3afa6a7d9 | ||
|
ce7e2e3243 | ||
|
13416825b1 | ||
|
6c664e7ba9 | ||
|
34359617b5 | ||
|
9e2696b9bc | ||
|
bf840e8bfa | ||
|
1f03c140f5 | ||
|
2de161ce0e | ||
|
1171106afb | ||
|
f57ae73071 | ||
|
59872b5698 | ||
|
7cd8ea00d1 | ||
|
4b2f38926a | ||
|
537c95cf29 | ||
|
81a5722708 | ||
|
c150b913ac | ||
|
3e4b67db6c | ||
|
d727f8ff50 | ||
|
9546bf1dee | ||
|
dd9ce34d18 | ||
|
73f2d972e4 | ||
|
7d699c6c35 | ||
|
21f23f67f4 | ||
|
8874ba2779 | ||
|
420538e6e7 | ||
|
8eb68b54d9 | ||
|
80202f33cb | ||
|
c24579bfb2 | ||
|
21256c4529 | ||
|
668626b920 | ||
|
cbfa3bb56d | ||
|
536fcf02d7 | ||
|
a8ac3acbbe | ||
|
7980155375 | ||
|
aa855e31c8 | ||
|
675ee8e813 | ||
|
50ccce7387 | ||
|
40b561ea69 | ||
|
a0f73bd30f | ||
|
1b7fcce42d | ||
|
4749af6e90 | ||
|
f7ad40263b | ||
|
e5b25bfa58 | ||
|
1d23adcda3 | ||
|
0216d36ab7 | ||
|
2bec20ad76 | ||
|
93c1245b0f | ||
|
72504d7619 | ||
|
320aa34d39 | ||
|
87f2a4242e | ||
|
9bf0cbd659 | ||
|
b1470fd9b8 | ||
|
08016dc3b6 | ||
|
7a448f5528 | ||
|
4ac23bf14c | ||
|
bc708dee30 | ||
|
2888e5748e | ||
|
88f0a33e69 | ||
|
3165f92b6b | ||
|
3bd0fca633 | ||
|
cdff10d281 | ||
|
e425741c34 | ||
|
20a367b243 | ||
|
fdded9e7ee | ||
|
7d29bff136 | ||
|
0abfbeed3c | ||
|
35b7c3038a | ||
|
46dd96a4b7 | ||
|
788232ca35 | ||
|
3b458738e0 | ||
|
2c8fc67ab1 | ||
|
9b3ed3ed72 | ||
|
c59197e87a | ||
|
03e3c88d8b | ||
|
39693786ef | ||
|
357c324df1 | ||
|
650482208c | ||
|
2acad4a78c | ||
|
65ee4e1916 | ||
|
275bbc81f0 | ||
|
beafcf74ab | ||
|
e47909bb3e | ||
|
0b3b9c2257 | ||
|
8fb7a7e4cd | ||
|
c5ed148c52 | ||
|
e774c710a8 | ||
|
d237180a98 | ||
|
d8b618f7c3 | ||
|
e888a95bd1 | ||
|
36c2404a46 | ||
|
ba673beb82 | ||
|
4b56701152 | ||
|
59227116f3 | ||
|
9b0975b2ac | ||
|
3a39a5caa3 | ||
|
93e270f379 | ||
|
98c81fa2af | ||
|
1bb32a05a9 | ||
|
5dd4b77270 | ||
|
737d1aac7c | ||
|
886feae4ca | ||
|
1dfe26f14f | ||
|
d66fcd23df | ||
|
bdfb47e999 | ||
|
10300cc478 | ||
|
ababa639b3 | ||
|
9f6569d658 | ||
|
24c22ebdc7 | ||
|
6c365fffde | ||
|
dbb80dd6c0 | ||
|
624834de9c | ||
|
d31995f878 | ||
|
017b1cae26 | ||
|
c09f15b0e9 | ||
|
68284bed74 | ||
|
9a44d668d6 | ||
|
67e0197a7a | ||
|
a5a8cfa17d | ||
|
60c3e701e9 | ||
|
b9b129dcf5 | ||
|
d882ab236a | ||
|
140cc0e486 | ||
|
6ac7c0f893 | ||
|
096d50617f | ||
|
9dd8c0cc4f | ||
|
de0fab86ec | ||
|
bb36dd3893 | ||
|
ada837ee95 | ||
|
67e73173f6 | ||
|
4b63829eef | ||
|
029411d3fa | ||
|
6ba033f934 | ||
|
3734fa948f | ||
|
336742e335 | ||
|
66ca424d3a | ||
|
2da0a91a36 | ||
|
fee1bde231 | ||
|
4a94430bf0 | ||
|
cc337f7b1e | ||
|
d8a06777fe | ||
|
9207eedbfb | ||
|
c97b832648 | ||
|
4ef629f79d | ||
|
0b4e3c3db5 | ||
|
f12cc523b4 | ||
|
5c3c9d2ed1 | ||
|
3ac3673326 | ||
|
1a3940575e | ||
|
16c8b1efab | ||
|
0e789be09f | ||
|
a948c7d69d | ||
|
d8ec0103a9 | ||
|
50161670ce | ||
|
c1f612dce1 | ||
|
6fb74482d7 | ||
|
4b680ffa5f | ||
|
c71c8d56ce | ||
|
295ae7b4bc | ||
|
839c884cef | ||
|
13ffe7acfb | ||
|
39a0c0d96e | ||
|
a95a542148 | ||
|
b3cb2ac3ee | ||
|
759fe54132 | ||
|
519a888e82 | ||
|
4f1e4e7471 | ||
|
7b8a32f630 | ||
|
92d91a65bb | ||
|
dab5289177 | ||
|
a77cb1e579 | ||
|
01bdda0ae6 | ||
|
fbe35e6e6b | ||
|
a3cd74e30b | ||
|
dbd4781de1 | ||
|
6d48316436 | ||
|
cca6965cd1 | ||
|
dd63ed7e69 | ||
|
61e2283146 | ||
|
97eb768748 | ||
|
be8b5a8aeb | ||
|
99ed39b26c | ||
|
48a0eb90a7 | ||
|
3c342077d6 | ||
|
f1bef1e7e6 | ||
|
da9749ecce | ||
|
fa7be597d2 | ||
|
53da418d68 | ||
|
897ed7e381 | ||
|
daf0939f09 | ||
|
7b1d6ddcf6 | ||
|
267e1dd0f8 | ||
|
c9d0bfce54 | ||
|
7f9e5e29a8 | ||
|
d0f685183d | ||
|
bed77bd356 | ||
|
bc0e3b254b | ||
|
47bf0ebb47 | ||
|
0acb95bbd5 | ||
|
8665f4a251 | ||
|
3adacb8799 | ||
|
76aa69b9ac | ||
|
78116f1596 | ||
|
36693b7d9d | ||
|
8ce68f93ea | ||
|
3512cb9599 | ||
|
ea164a2030 | ||
|
929ba70ef8 | ||
|
5b2113c43d | ||
|
6df2c0bab5 | ||
|
1c5193aa4d | ||
|
bd55fe868d | ||
|
87a2465a25 | ||
|
5f839ad3ee | ||
|
1663d8dfa9 | ||
|
08eafc54e6 | ||
|
fe1d8b137e | ||
|
39c0826f3c | ||
|
bf63b0993d | ||
|
f91a1363cb | ||
|
a2c9aa7662 | ||
|
d135da6c1d | ||
|
d27051f04d | ||
|
b28fa2a1ad | ||
|
77a91f5a8f | ||
|
dcc7ee98b3 | ||
|
30edb2a44f | ||
|
f63332a7aa | ||
|
86c37ce192 | ||
|
93e6c9e5a0 | ||
|
92e1fa4d3a | ||
|
bf7d292884 | ||
|
add8db0186 | ||
|
3e62c6ae2f | ||
|
cd4aa8ccd6 | ||
|
937dbdc71f | ||
|
66a7b508b2 | ||
|
a5493f7947 | ||
|
979c4907da | ||
|
b8f6fdeb2b | ||
|
067376cb3b | ||
|
bdbe9255a6 | ||
|
c460e1bbbe | ||
|
7e2b72fa5e | ||
|
6ee6a8a74f | ||
|
80984c94a1 | ||
|
1757b66467 | ||
|
8aa25af014 | ||
|
5a0e47be48 | ||
|
756a866ffd | ||
|
29305be23b | ||
|
8253cfd21d | ||
|
165a00896e | ||
|
2149ea1306 | ||
|
90547da007 | ||
|
9ec4881d8d | ||
|
487593af38 | ||
|
4e8f878d83 | ||
|
af6544c64d | ||
|
09e1f53b3e | ||
|
1c4f191f42 | ||
|
a37bd824d5 | ||
|
2c79173d20 | ||
|
eb45b89557 | ||
|
bf8c345341 | ||
|
ef46280716 | ||
|
2453e1284f | ||
|
95bcb272e0 | ||
|
e0e61b5262 | ||
|
3ddef56167 | ||
|
f8e6fb81d6 | ||
|
683ec87adf | ||
|
23edbe5ce7 | ||
|
6ff32a51e3 | ||
|
4cbac3a864 | ||
|
94a99b5bec | ||
|
810bf06e16 | ||
|
1254667b2c | ||
|
053eb8a0fd | ||
|
82ef380256 | ||
|
44449d8e72 | ||
|
6c3a0890c7 | ||
|
8c0def7c79 | ||
|
de77751779 | ||
|
cdf809926b | ||
|
d40341f1ad | ||
|
4a94fb91d7 | ||
|
24ea9ca947 | ||
|
98eb9bf2bd | ||
|
1eb30cf3ab | ||
|
6fd7c0ff8e | ||
|
263e81cb2c | ||
|
92ebf37d86 | ||
|
a10e406131 | ||
|
21095e80a7 | ||
|
55ae43ed03 | ||
|
9cc934a972 | ||
|
cdfec7ebb4 | ||
|
59ad69b637 | ||
|
ca6b759607 | ||
|
f9d857211f | ||
|
01ad8661d6 | ||
|
d21b8166f0 | ||
|
63582bb489 | ||
|
c19f2de3a8 | ||
|
d2e7b61eb2 | ||
|
13a448ebfe | ||
|
bad2e1f9c4 | ||
|
8edac51401 | ||
|
f34ba9bf96 | ||
|
82aea946a2 | ||
|
a0665dc431 | ||
|
e32d6cdecd | ||
|
23b43319a8 |
1887 changed files with 78616 additions and 20104 deletions
|
@ -79,6 +79,7 @@ components: &components
|
||||||
- homeassistant/components/group/**
|
- homeassistant/components/group/**
|
||||||
- homeassistant/components/hassio/**
|
- homeassistant/components/hassio/**
|
||||||
- homeassistant/components/homeassistant/**
|
- homeassistant/components/homeassistant/**
|
||||||
|
- homeassistant/components/homeassistant_hardware/**
|
||||||
- homeassistant/components/http/**
|
- homeassistant/components/http/**
|
||||||
- homeassistant/components/image/**
|
- homeassistant/components/image/**
|
||||||
- homeassistant/components/input_boolean/**
|
- homeassistant/components/input_boolean/**
|
||||||
|
|
|
@ -58,7 +58,13 @@
|
||||||
],
|
],
|
||||||
"[python]": {
|
"[python]": {
|
||||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||||
}
|
},
|
||||||
|
"json.schemas": [
|
||||||
|
{
|
||||||
|
"fileMatch": ["homeassistant/components/*/manifest.json"],
|
||||||
|
"url": "./script/json_schemas/manifest_schema.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
|
@ -1,2 +1 @@
|
||||||
custom: https://www.nabucasa.com
|
custom: https://www.openhomefoundation.org
|
||||||
github: balloob
|
|
||||||
|
|
24
.github/workflows/builder.yml
vendored
24
.github/workflows/builder.yml
vendored
|
@ -10,7 +10,7 @@ on:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_TYPE: core
|
BUILD_TYPE: core
|
||||||
DEFAULT_PYTHON: "3.12"
|
DEFAULT_PYTHON: "3.13"
|
||||||
PIP_TIMEOUT: 60
|
PIP_TIMEOUT: 60
|
||||||
UV_HTTP_TIMEOUT: 60
|
UV_HTTP_TIMEOUT: 60
|
||||||
UV_SYSTEM_PYTHON: "true"
|
UV_SYSTEM_PYTHON: "true"
|
||||||
|
@ -27,12 +27,12 @@ jobs:
|
||||||
publish: ${{ steps.version.outputs.publish }}
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ jobs:
|
||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
|
@ -116,7 +116,7 @@ jobs:
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
@ -242,7 +242,7 @@ jobs:
|
||||||
- green
|
- green
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
|
@ -279,7 +279,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
uses: home-assistant/actions/helpers/git-init@master
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
|
@ -321,7 +321,7 @@ jobs:
|
||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.7.0
|
uses: sigstore/cosign-installer@v3.7.0
|
||||||
|
@ -451,10 +451,10 @@ jobs:
|
||||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
@ -499,7 +499,7 @@ jobs:
|
||||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
|
@ -531,7 +531,7 @@ jobs:
|
||||||
|
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3
|
uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
|
166
.github/workflows/ci.yaml
vendored
166
.github/workflows/ci.yaml
vendored
|
@ -40,9 +40,9 @@ env:
|
||||||
CACHE_VERSION: 11
|
CACHE_VERSION: 11
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 9
|
MYPY_CACHE_VERSION: 9
|
||||||
HA_SHORT_VERSION: "2024.11"
|
HA_SHORT_VERSION: "2024.12"
|
||||||
DEFAULT_PYTHON: "3.12"
|
DEFAULT_PYTHON: "3.12"
|
||||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
ALL_PYTHON_VERSIONS: "['3.12', '3.13']"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||||
# 10.6 is the current long-term-support
|
# 10.6 is the current long-term-support
|
||||||
|
@ -93,7 +93,7 @@ jobs:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Generate partial Python venv restore key
|
- name: Generate partial Python venv restore key
|
||||||
id: generate_python_cache_key
|
id: generate_python_cache_key
|
||||||
run: |
|
run: |
|
||||||
|
@ -231,16 +231,16 @@ jobs:
|
||||||
- info
|
- info
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.1.1
|
uses: actions/cache@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: >-
|
key: >-
|
||||||
|
@ -256,7 +256,7 @@ jobs:
|
||||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v4.1.1
|
uses: actions/cache@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
|
@ -277,16 +277,16 @@ jobs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -295,7 +295,7 @@ jobs:
|
||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -317,16 +317,16 @@ jobs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -335,7 +335,7 @@ jobs:
|
||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -357,16 +357,16 @@ jobs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -375,7 +375,7 @@ jobs:
|
||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -447,7 +447,7 @@ jobs:
|
||||||
- script/hassfest/docker/Dockerfile
|
- script/hassfest/docker/Dockerfile
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Register hadolint problem matcher
|
- name: Register hadolint problem matcher
|
||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||||
|
@ -466,10 +466,10 @@ jobs:
|
||||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
@ -482,7 +482,7 @@ jobs:
|
||||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.1.1
|
uses: actions/cache@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
|
@ -491,7 +491,7 @@ jobs:
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Restore uv wheel cache
|
- name: Restore uv wheel cache
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache@v4.1.1
|
uses: actions/cache@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.UV_CACHE_DIR }}
|
path: ${{ env.UV_CACHE_DIR }}
|
||||||
key: >-
|
key: >-
|
||||||
|
@ -550,16 +550,16 @@ jobs:
|
||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
libturbojpeg
|
libturbojpeg
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -583,16 +583,16 @@ jobs:
|
||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -615,37 +615,41 @@ jobs:
|
||||||
&& github.event.inputs.mypy-only != 'true'
|
&& github.event.inputs.mypy-only != 'true'
|
||||||
|| github.event.inputs.audit-licenses-only == 'true')
|
|| github.event.inputs.audit-licenses-only == 'true')
|
||||||
&& needs.info.outputs.requirements == 'true'
|
&& needs.info.outputs.requirements == 'true'
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Run pip-licenses
|
- name: Extract license data
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pip-licenses --format=json --output-file=licenses.json
|
python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json
|
||||||
- name: Upload licenses
|
- name: Upload licenses
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: licenses
|
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
||||||
path: licenses.json
|
path: licenses-${{ matrix.python-version }}.json
|
||||||
- name: Process licenses
|
- name: Check licenses
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python -m script.licenses licenses.json
|
python -m script.licenses check licenses-${{ matrix.python-version }}.json
|
||||||
|
|
||||||
pylint:
|
pylint:
|
||||||
name: Check pylint
|
name: Check pylint
|
||||||
|
@ -660,16 +664,16 @@ jobs:
|
||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -707,16 +711,16 @@ jobs:
|
||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -752,10 +756,10 @@ jobs:
|
||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
@ -768,7 +772,7 @@ jobs:
|
||||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -776,7 +780,7 @@ jobs:
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Restore mypy cache
|
- name: Restore mypy cache
|
||||||
uses: actions/cache@v4.1.1
|
uses: actions/cache@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: .mypy_cache
|
path: .mypy_cache
|
||||||
key: >-
|
key: >-
|
||||||
|
@ -815,11 +819,7 @@ jobs:
|
||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
- base
|
- base
|
||||||
strategy:
|
name: Split tests for full run
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
|
||||||
name: Split tests for full run Python ${{ matrix.python-version }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install additional OS dependencies
|
- name: Install additional OS dependencies
|
||||||
run: |
|
run: |
|
||||||
|
@ -831,16 +831,16 @@ jobs:
|
||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -854,7 +854,7 @@ jobs:
|
||||||
- name: Upload pytest_buckets
|
- name: Upload pytest_buckets
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets-${{ matrix.python-version }}
|
name: pytest_buckets
|
||||||
path: pytest_buckets.txt
|
path: pytest_buckets.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
|
@ -895,16 +895,16 @@ jobs:
|
||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -919,7 +919,7 @@ jobs:
|
||||||
- name: Download pytest_buckets
|
- name: Download pytest_buckets
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets-${{ matrix.python-version }}
|
name: pytest_buckets
|
||||||
- name: Compile English translations
|
- name: Compile English translations
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
@ -945,6 +945,7 @@ jobs:
|
||||||
--timeout=9 \
|
--timeout=9 \
|
||||||
--durations=10 \
|
--durations=10 \
|
||||||
--numprocesses auto \
|
--numprocesses auto \
|
||||||
|
--snapshot-details \
|
||||||
--dist=loadfile \
|
--dist=loadfile \
|
||||||
${cov_params[@]} \
|
${cov_params[@]} \
|
||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
|
@ -1015,16 +1016,16 @@ jobs:
|
||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libmariadb-dev-compat
|
libmariadb-dev-compat
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -1067,6 +1068,7 @@ jobs:
|
||||||
-qq \
|
-qq \
|
||||||
--timeout=20 \
|
--timeout=20 \
|
||||||
--numprocesses 1 \
|
--numprocesses 1 \
|
||||||
|
--snapshot-details \
|
||||||
${cov_params[@]} \
|
${cov_params[@]} \
|
||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
--durations=10 \
|
--durations=10 \
|
||||||
|
@ -1098,7 +1100,7 @@ jobs:
|
||||||
./script/check_dirty
|
./script/check_dirty
|
||||||
|
|
||||||
pytest-postgres:
|
pytest-postgres:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: ${{ matrix.postgresql-group }}
|
image: ${{ matrix.postgresql-group }}
|
||||||
|
@ -1138,19 +1140,21 @@ jobs:
|
||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
bluez \
|
bluez \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
libturbojpeg \
|
libturbojpeg
|
||||||
|
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
|
||||||
|
sudo apt-get -y install \
|
||||||
postgresql-server-dev-14
|
postgresql-server-dev-14
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -1193,6 +1197,7 @@ jobs:
|
||||||
-qq \
|
-qq \
|
||||||
--timeout=9 \
|
--timeout=9 \
|
||||||
--numprocesses 1 \
|
--numprocesses 1 \
|
||||||
|
--snapshot-details \
|
||||||
${cov_params[@]} \
|
${cov_params[@]} \
|
||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
--durations=0 \
|
--durations=0 \
|
||||||
|
@ -1236,7 +1241,7 @@ jobs:
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
|
@ -1287,16 +1292,16 @@ jobs:
|
||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.1.1
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
@ -1339,6 +1344,7 @@ jobs:
|
||||||
-qq \
|
-qq \
|
||||||
--timeout=9 \
|
--timeout=9 \
|
||||||
--numprocesses auto \
|
--numprocesses auto \
|
||||||
|
--snapshot-details \
|
||||||
${cov_params[@]} \
|
${cov_params[@]} \
|
||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
--durations=0 \
|
--durations=0 \
|
||||||
|
@ -1374,7 +1380,7 @@ jobs:
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
|
|
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
|
@ -21,14 +21,14 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.26.13
|
uses: github/codeql-action/init@v3.27.3
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.26.13
|
uses: github/codeql-action/analyze@v3.27.3
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
|
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
|
@ -19,10 +19,10 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
|
38
.github/workflows/wheels.yml
vendored
38
.github/workflows/wheels.yml
vendored
|
@ -32,11 +32,11 @@ jobs:
|
||||||
architectures: ${{ steps.info.outputs.architectures }}
|
architectures: ${{ steps.info.outputs.architectures }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
@ -112,11 +112,11 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
abi: ["cp312"]
|
abi: ["cp312", "cp313"]
|
||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
|
@ -135,14 +135,14 @@ jobs:
|
||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2024.07.1
|
uses: home-assistant/wheels@2024.11.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "libffi-dev;openssl-dev;yaml-dev;nasm"
|
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev"
|
||||||
skip-binary: aiohttp;multidict;yarl
|
skip-binary: aiohttp;multidict;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
|
@ -156,11 +156,11 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
abi: ["cp312"]
|
abi: ["cp312", "cp313"]
|
||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.1
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
|
@ -198,6 +198,7 @@ jobs:
|
||||||
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt
|
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt
|
||||||
|
|
||||||
- name: Create requirements for cython<3
|
- name: Create requirements for cython<3
|
||||||
|
if: matrix.abi == 'cp312'
|
||||||
run: |
|
run: |
|
||||||
# Some dependencies still require 'cython<3'
|
# Some dependencies still require 'cython<3'
|
||||||
# and don't yet use isolated build environments.
|
# and don't yet use isolated build environments.
|
||||||
|
@ -208,7 +209,8 @@ jobs:
|
||||||
cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt
|
cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt
|
||||||
|
|
||||||
- name: Build wheels (old cython)
|
- name: Build wheels (old cython)
|
||||||
uses: home-assistant/wheels@2024.07.1
|
uses: home-assistant/wheels@2024.11.0
|
||||||
|
if: matrix.abi == 'cp312'
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
|
@ -223,43 +225,43 @@ jobs:
|
||||||
pip: "'cython<3'"
|
pip: "'cython<3'"
|
||||||
|
|
||||||
- name: Build wheels (part 1)
|
- name: Build wheels (part 1)
|
||||||
uses: home-assistant/wheels@2024.07.1
|
uses: home-assistant/wheels@2024.11.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtaa"
|
requirements: "requirements_all.txtaa"
|
||||||
|
|
||||||
- name: Build wheels (part 2)
|
- name: Build wheels (part 2)
|
||||||
uses: home-assistant/wheels@2024.07.1
|
uses: home-assistant/wheels@2024.11.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtab"
|
requirements: "requirements_all.txtab"
|
||||||
|
|
||||||
- name: Build wheels (part 3)
|
- name: Build wheels (part 3)
|
||||||
uses: home-assistant/wheels@2024.07.1
|
uses: home-assistant/wheels@2024.11.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtac"
|
requirements: "requirements_all.txtac"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.7.0
|
rev: v0.7.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
|
@ -90,7 +90,7 @@ repos:
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
types: [text]
|
||||||
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml)$
|
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$
|
||||||
- id: hassfest-mypy-config
|
- id: hassfest-mypy-config
|
||||||
name: hassfest-mypy-config
|
name: hassfest-mypy-config
|
||||||
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config
|
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config
|
||||||
|
|
|
@ -124,6 +124,7 @@ homeassistant.components.bryant_evolution.*
|
||||||
homeassistant.components.bthome.*
|
homeassistant.components.bthome.*
|
||||||
homeassistant.components.button.*
|
homeassistant.components.button.*
|
||||||
homeassistant.components.calendar.*
|
homeassistant.components.calendar.*
|
||||||
|
homeassistant.components.cambridge_audio.*
|
||||||
homeassistant.components.camera.*
|
homeassistant.components.camera.*
|
||||||
homeassistant.components.canary.*
|
homeassistant.components.canary.*
|
||||||
homeassistant.components.cert_expiry.*
|
homeassistant.components.cert_expiry.*
|
||||||
|
@ -208,6 +209,7 @@ homeassistant.components.geo_location.*
|
||||||
homeassistant.components.geocaching.*
|
homeassistant.components.geocaching.*
|
||||||
homeassistant.components.gios.*
|
homeassistant.components.gios.*
|
||||||
homeassistant.components.glances.*
|
homeassistant.components.glances.*
|
||||||
|
homeassistant.components.go2rtc.*
|
||||||
homeassistant.components.goalzero.*
|
homeassistant.components.goalzero.*
|
||||||
homeassistant.components.google.*
|
homeassistant.components.google.*
|
||||||
homeassistant.components.google_assistant_sdk.*
|
homeassistant.components.google_assistant_sdk.*
|
||||||
|
@ -322,11 +324,13 @@ homeassistant.components.moon.*
|
||||||
homeassistant.components.mopeka.*
|
homeassistant.components.mopeka.*
|
||||||
homeassistant.components.motionmount.*
|
homeassistant.components.motionmount.*
|
||||||
homeassistant.components.mqtt.*
|
homeassistant.components.mqtt.*
|
||||||
|
homeassistant.components.music_assistant.*
|
||||||
homeassistant.components.my.*
|
homeassistant.components.my.*
|
||||||
homeassistant.components.mysensors.*
|
homeassistant.components.mysensors.*
|
||||||
homeassistant.components.myuplink.*
|
homeassistant.components.myuplink.*
|
||||||
homeassistant.components.nam.*
|
homeassistant.components.nam.*
|
||||||
homeassistant.components.nanoleaf.*
|
homeassistant.components.nanoleaf.*
|
||||||
|
homeassistant.components.nasweb.*
|
||||||
homeassistant.components.neato.*
|
homeassistant.components.neato.*
|
||||||
homeassistant.components.nest.*
|
homeassistant.components.nest.*
|
||||||
homeassistant.components.netatmo.*
|
homeassistant.components.netatmo.*
|
||||||
|
@ -336,6 +340,7 @@ homeassistant.components.nfandroidtv.*
|
||||||
homeassistant.components.nightscout.*
|
homeassistant.components.nightscout.*
|
||||||
homeassistant.components.nissan_leaf.*
|
homeassistant.components.nissan_leaf.*
|
||||||
homeassistant.components.no_ip.*
|
homeassistant.components.no_ip.*
|
||||||
|
homeassistant.components.nordpool.*
|
||||||
homeassistant.components.notify.*
|
homeassistant.components.notify.*
|
||||||
homeassistant.components.notion.*
|
homeassistant.components.notion.*
|
||||||
homeassistant.components.number.*
|
homeassistant.components.number.*
|
||||||
|
|
10
.vscode/settings.default.json
vendored
10
.vscode/settings.default.json
vendored
|
@ -6,5 +6,13 @@
|
||||||
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
|
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
|
||||||
"python.testing.pytestEnabled": false,
|
"python.testing.pytestEnabled": false,
|
||||||
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
||||||
"pylint.importStrategy": "fromEnvironment"
|
"pylint.importStrategy": "fromEnvironment",
|
||||||
|
"json.schemas": [
|
||||||
|
{
|
||||||
|
"fileMatch": [
|
||||||
|
"homeassistant/components/*/manifest.json"
|
||||||
|
],
|
||||||
|
"url": "./script/json_schemas/manifest_schema.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
34
CODEOWNERS
34
CODEOWNERS
|
@ -40,6 +40,8 @@ build.json @home-assistant/supervisor
|
||||||
# Integrations
|
# Integrations
|
||||||
/homeassistant/components/abode/ @shred86
|
/homeassistant/components/abode/ @shred86
|
||||||
/tests/components/abode/ @shred86
|
/tests/components/abode/ @shred86
|
||||||
|
/homeassistant/components/acaia/ @zweckj
|
||||||
|
/tests/components/acaia/ @zweckj
|
||||||
/homeassistant/components/accuweather/ @bieniu
|
/homeassistant/components/accuweather/ @bieniu
|
||||||
/tests/components/accuweather/ @bieniu
|
/tests/components/accuweather/ @bieniu
|
||||||
/homeassistant/components/acmeda/ @atmurray
|
/homeassistant/components/acmeda/ @atmurray
|
||||||
|
@ -496,8 +498,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/freebox/ @hacf-fr @Quentame
|
/tests/components/freebox/ @hacf-fr @Quentame
|
||||||
/homeassistant/components/freedompro/ @stefano055415
|
/homeassistant/components/freedompro/ @stefano055415
|
||||||
/tests/components/freedompro/ @stefano055415
|
/tests/components/freedompro/ @stefano055415
|
||||||
/homeassistant/components/fritz/ @mammuth @AaronDavidSchneider @chemelli74 @mib1185
|
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||||
/tests/components/fritz/ @mammuth @AaronDavidSchneider @chemelli74 @mib1185
|
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||||
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
||||||
/tests/components/fritzbox/ @mib1185 @flabbamann
|
/tests/components/fritzbox/ @mib1185 @flabbamann
|
||||||
/homeassistant/components/fritzbox_callmonitor/ @cdce8p
|
/homeassistant/components/fritzbox_callmonitor/ @cdce8p
|
||||||
|
@ -617,8 +619,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/hlk_sw16/ @jameshilliard
|
/tests/components/hlk_sw16/ @jameshilliard
|
||||||
/homeassistant/components/holiday/ @jrieger @gjohansson-ST
|
/homeassistant/components/holiday/ @jrieger @gjohansson-ST
|
||||||
/tests/components/holiday/ @jrieger @gjohansson-ST
|
/tests/components/holiday/ @jrieger @gjohansson-ST
|
||||||
/homeassistant/components/home_connect/ @DavidMStraub
|
/homeassistant/components/home_connect/ @DavidMStraub @Diegorro98
|
||||||
/tests/components/home_connect/ @DavidMStraub
|
/tests/components/home_connect/ @DavidMStraub @Diegorro98
|
||||||
/homeassistant/components/homeassistant/ @home-assistant/core
|
/homeassistant/components/homeassistant/ @home-assistant/core
|
||||||
/tests/components/homeassistant/ @home-assistant/core
|
/tests/components/homeassistant/ @home-assistant/core
|
||||||
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
|
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
|
||||||
|
@ -659,6 +661,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||||
/homeassistant/components/husqvarna_automower/ @Thomas55555
|
/homeassistant/components/husqvarna_automower/ @Thomas55555
|
||||||
/tests/components/husqvarna_automower/ @Thomas55555
|
/tests/components/husqvarna_automower/ @Thomas55555
|
||||||
|
/homeassistant/components/husqvarna_automower_ble/ @alistair23
|
||||||
|
/tests/components/husqvarna_automower_ble/ @alistair23
|
||||||
/homeassistant/components/huum/ @frwickst
|
/homeassistant/components/huum/ @frwickst
|
||||||
/tests/components/huum/ @frwickst
|
/tests/components/huum/ @frwickst
|
||||||
/homeassistant/components/hvv_departures/ @vigonotion
|
/homeassistant/components/hvv_departures/ @vigonotion
|
||||||
|
@ -819,6 +823,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/lektrico/ @lektrico
|
/tests/components/lektrico/ @lektrico
|
||||||
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
||||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||||
|
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
|
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
/homeassistant/components/lidarr/ @tkdrob
|
/homeassistant/components/lidarr/ @tkdrob
|
||||||
/tests/components/lidarr/ @tkdrob
|
/tests/components/lidarr/ @tkdrob
|
||||||
/homeassistant/components/lifx/ @Djelibeybi
|
/homeassistant/components/lifx/ @Djelibeybi
|
||||||
|
@ -950,6 +956,8 @@ build.json @home-assistant/supervisor
|
||||||
/homeassistant/components/msteams/ @peroyvind
|
/homeassistant/components/msteams/ @peroyvind
|
||||||
/homeassistant/components/mullvad/ @meichthys
|
/homeassistant/components/mullvad/ @meichthys
|
||||||
/tests/components/mullvad/ @meichthys
|
/tests/components/mullvad/ @meichthys
|
||||||
|
/homeassistant/components/music_assistant/ @music-assistant
|
||||||
|
/tests/components/music_assistant/ @music-assistant
|
||||||
/homeassistant/components/mutesync/ @currentoor
|
/homeassistant/components/mutesync/ @currentoor
|
||||||
/tests/components/mutesync/ @currentoor
|
/tests/components/mutesync/ @currentoor
|
||||||
/homeassistant/components/my/ @home-assistant/core
|
/homeassistant/components/my/ @home-assistant/core
|
||||||
|
@ -964,6 +972,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/nam/ @bieniu
|
/tests/components/nam/ @bieniu
|
||||||
/homeassistant/components/nanoleaf/ @milanmeu @joostlek
|
/homeassistant/components/nanoleaf/ @milanmeu @joostlek
|
||||||
/tests/components/nanoleaf/ @milanmeu @joostlek
|
/tests/components/nanoleaf/ @milanmeu @joostlek
|
||||||
|
/homeassistant/components/nasweb/ @nasWebio
|
||||||
|
/tests/components/nasweb/ @nasWebio
|
||||||
/homeassistant/components/neato/ @Santobert
|
/homeassistant/components/neato/ @Santobert
|
||||||
/tests/components/neato/ @Santobert
|
/tests/components/neato/ @Santobert
|
||||||
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM
|
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM
|
||||||
|
@ -1004,6 +1014,8 @@ build.json @home-assistant/supervisor
|
||||||
/homeassistant/components/noaa_tides/ @jdelaney72
|
/homeassistant/components/noaa_tides/ @jdelaney72
|
||||||
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
|
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
|
||||||
/tests/components/nobo_hub/ @echoromeo @oyvindwe
|
/tests/components/nobo_hub/ @echoromeo @oyvindwe
|
||||||
|
/homeassistant/components/nordpool/ @gjohansson-ST
|
||||||
|
/tests/components/nordpool/ @gjohansson-ST
|
||||||
/homeassistant/components/notify/ @home-assistant/core
|
/homeassistant/components/notify/ @home-assistant/core
|
||||||
/tests/components/notify/ @home-assistant/core
|
/tests/components/notify/ @home-assistant/core
|
||||||
/homeassistant/components/notify_events/ @matrozov @papajojo
|
/homeassistant/components/notify_events/ @matrozov @papajojo
|
||||||
|
@ -1047,6 +1059,7 @@ build.json @home-assistant/supervisor
|
||||||
/homeassistant/components/onewire/ @garbled1 @epenet
|
/homeassistant/components/onewire/ @garbled1 @epenet
|
||||||
/tests/components/onewire/ @garbled1 @epenet
|
/tests/components/onewire/ @garbled1 @epenet
|
||||||
/homeassistant/components/onkyo/ @arturpragacz
|
/homeassistant/components/onkyo/ @arturpragacz
|
||||||
|
/tests/components/onkyo/ @arturpragacz
|
||||||
/homeassistant/components/onvif/ @hunterjm
|
/homeassistant/components/onvif/ @hunterjm
|
||||||
/tests/components/onvif/ @hunterjm
|
/tests/components/onvif/ @hunterjm
|
||||||
/homeassistant/components/open_meteo/ @frenck
|
/homeassistant/components/open_meteo/ @frenck
|
||||||
|
@ -1088,6 +1101,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/ovo_energy/ @timmo001
|
/tests/components/ovo_energy/ @timmo001
|
||||||
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
||||||
/tests/components/p1_monitor/ @klaasnicolaas
|
/tests/components/p1_monitor/ @klaasnicolaas
|
||||||
|
/homeassistant/components/palazzetti/ @dotvav
|
||||||
|
/tests/components/palazzetti/ @dotvav
|
||||||
/homeassistant/components/panel_custom/ @home-assistant/frontend
|
/homeassistant/components/panel_custom/ @home-assistant/frontend
|
||||||
/tests/components/panel_custom/ @home-assistant/frontend
|
/tests/components/panel_custom/ @home-assistant/frontend
|
||||||
/homeassistant/components/peco/ @IceBotYT
|
/homeassistant/components/peco/ @IceBotYT
|
||||||
|
@ -1331,6 +1346,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/siren/ @home-assistant/core @raman325
|
/tests/components/siren/ @home-assistant/core @raman325
|
||||||
/homeassistant/components/sisyphus/ @jkeljo
|
/homeassistant/components/sisyphus/ @jkeljo
|
||||||
/homeassistant/components/sky_hub/ @rogerselwyn
|
/homeassistant/components/sky_hub/ @rogerselwyn
|
||||||
|
/homeassistant/components/sky_remote/ @dunnmj @saty9
|
||||||
|
/tests/components/sky_remote/ @dunnmj @saty9
|
||||||
/homeassistant/components/skybell/ @tkdrob
|
/homeassistant/components/skybell/ @tkdrob
|
||||||
/tests/components/skybell/ @tkdrob
|
/tests/components/skybell/ @tkdrob
|
||||||
/homeassistant/components/slack/ @tkdrob @fletcherau
|
/homeassistant/components/slack/ @tkdrob @fletcherau
|
||||||
|
@ -1349,6 +1366,7 @@ build.json @home-assistant/supervisor
|
||||||
/homeassistant/components/smarttub/ @mdz
|
/homeassistant/components/smarttub/ @mdz
|
||||||
/tests/components/smarttub/ @mdz
|
/tests/components/smarttub/ @mdz
|
||||||
/homeassistant/components/smarty/ @z0mbieprocess
|
/homeassistant/components/smarty/ @z0mbieprocess
|
||||||
|
/tests/components/smarty/ @z0mbieprocess
|
||||||
/homeassistant/components/smhi/ @gjohansson-ST
|
/homeassistant/components/smhi/ @gjohansson-ST
|
||||||
/tests/components/smhi/ @gjohansson-ST
|
/tests/components/smhi/ @gjohansson-ST
|
||||||
/homeassistant/components/smlight/ @tl-sl
|
/homeassistant/components/smlight/ @tl-sl
|
||||||
|
@ -1412,8 +1430,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/stt/ @home-assistant/core
|
/tests/components/stt/ @home-assistant/core
|
||||||
/homeassistant/components/subaru/ @G-Two
|
/homeassistant/components/subaru/ @G-Two
|
||||||
/tests/components/subaru/ @G-Two
|
/tests/components/subaru/ @G-Two
|
||||||
/homeassistant/components/suez_water/ @ooii
|
/homeassistant/components/suez_water/ @ooii @jb101010-2
|
||||||
/tests/components/suez_water/ @ooii
|
/tests/components/suez_water/ @ooii @jb101010-2
|
||||||
/homeassistant/components/sun/ @Swamp-Ig
|
/homeassistant/components/sun/ @Swamp-Ig
|
||||||
/tests/components/sun/ @Swamp-Ig
|
/tests/components/sun/ @Swamp-Ig
|
||||||
/homeassistant/components/sunweg/ @rokam
|
/homeassistant/components/sunweg/ @rokam
|
||||||
|
@ -1471,8 +1489,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/tedee/ @patrickhilker @zweckj
|
/tests/components/tedee/ @patrickhilker @zweckj
|
||||||
/homeassistant/components/tellduslive/ @fredrike
|
/homeassistant/components/tellduslive/ @fredrike
|
||||||
/tests/components/tellduslive/ @fredrike
|
/tests/components/tellduslive/ @fredrike
|
||||||
/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
/homeassistant/components/template/ @PhracturedBlue @home-assistant/core
|
||||||
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
/tests/components/template/ @PhracturedBlue @home-assistant/core
|
||||||
/homeassistant/components/tesla_fleet/ @Bre77
|
/homeassistant/components/tesla_fleet/ @Bre77
|
||||||
/tests/components/tesla_fleet/ @Bre77
|
/tests/components/tesla_fleet/ @Bre77
|
||||||
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
||||||
|
|
|
@ -7,12 +7,13 @@ FROM ${BUILD_FROM}
|
||||||
# Synchronize with homeassistant/core.py:async_stop
|
# Synchronize with homeassistant/core.py:async_stop
|
||||||
ENV \
|
ENV \
|
||||||
S6_SERVICES_GRACETIME=240000 \
|
S6_SERVICES_GRACETIME=240000 \
|
||||||
UV_SYSTEM_PYTHON=true
|
UV_SYSTEM_PYTHON=true \
|
||||||
|
UV_NO_CACHE=true
|
||||||
|
|
||||||
ARG QEMU_CPU
|
ARG QEMU_CPU
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
RUN pip3 install uv==0.4.22
|
RUN pip3 install uv==0.5.0
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
@ -54,7 +55,7 @@ RUN \
|
||||||
"armv7") go2rtc_suffix='arm' ;; \
|
"armv7") go2rtc_suffix='arm' ;; \
|
||||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||||
esac \
|
esac \
|
||||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.4/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.7/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||||
&& chmod +x /bin/go2rtc \
|
&& chmod +x /bin/go2rtc \
|
||||||
# Verify go2rtc can be executed
|
# Verify go2rtc can be executed
|
||||||
&& go2rtc --version
|
&& go2rtc --version
|
||||||
|
|
|
@ -35,6 +35,9 @@ RUN \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Add go2rtc binary
|
||||||
|
COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
RUN pip3 install uv
|
RUN pip3 install uv
|
||||||
|
|
||||||
|
|
10
build.yaml
10
build.yaml
|
@ -1,10 +1,10 @@
|
||||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.06.1
|
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.11.0
|
||||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.06.1
|
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.11.0
|
||||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.06.1
|
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.11.0
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.06.1
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.11.0
|
||||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.06.1
|
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.11.0
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
|
|
|
@ -9,6 +9,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
from .backup_restore import restore_backup
|
||||||
from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||||
|
|
||||||
FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
||||||
|
@ -182,6 +183,9 @@ def main() -> int:
|
||||||
return scripts.run(args.script)
|
return scripts.run(args.script)
|
||||||
|
|
||||||
config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config))
|
config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config))
|
||||||
|
if restore_backup(config_dir):
|
||||||
|
return RESTART_EXIT_CODE
|
||||||
|
|
||||||
ensure_config_path(config_dir)
|
ensure_config_path(config_dir)
|
||||||
|
|
||||||
# pylint: disable-next=import-outside-toplevel
|
# pylint: disable-next=import-outside-toplevel
|
||||||
|
|
126
homeassistant/backup_restore.py
Normal file
126
homeassistant/backup_restore.py
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
"""Home Assistant module to handle restoring backups."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
from awesomeversion import AwesomeVersion
|
||||||
|
import securetar
|
||||||
|
|
||||||
|
from .const import __version__ as HA_VERSION
|
||||||
|
|
||||||
|
RESTORE_BACKUP_FILE = ".HA_RESTORE"
|
||||||
|
KEEP_PATHS = ("backups",)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RestoreBackupFileContent:
|
||||||
|
"""Definition for restore backup file content."""
|
||||||
|
|
||||||
|
backup_file_path: Path
|
||||||
|
|
||||||
|
|
||||||
|
def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent | None:
|
||||||
|
"""Return the contents of the restore backup file."""
|
||||||
|
instruction_path = config_dir.joinpath(RESTORE_BACKUP_FILE)
|
||||||
|
try:
|
||||||
|
instruction_content = json.loads(instruction_path.read_text(encoding="utf-8"))
|
||||||
|
return RestoreBackupFileContent(
|
||||||
|
backup_file_path=Path(instruction_content["path"])
|
||||||
|
)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_configuration_directory(config_dir: Path) -> None:
|
||||||
|
"""Delete all files and directories in the config directory except for the backups directory."""
|
||||||
|
keep_paths = [config_dir.joinpath(path) for path in KEEP_PATHS]
|
||||||
|
config_contents = sorted(
|
||||||
|
[entry for entry in config_dir.iterdir() if entry not in keep_paths]
|
||||||
|
)
|
||||||
|
|
||||||
|
for entry in config_contents:
|
||||||
|
entrypath = config_dir.joinpath(entry)
|
||||||
|
|
||||||
|
if entrypath.is_file():
|
||||||
|
entrypath.unlink()
|
||||||
|
elif entrypath.is_dir():
|
||||||
|
shutil.rmtree(entrypath)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_backup(config_dir: Path, backup_file_path: Path) -> None:
|
||||||
|
"""Extract the backup file to the config directory."""
|
||||||
|
with (
|
||||||
|
TemporaryDirectory() as tempdir,
|
||||||
|
securetar.SecureTarFile(
|
||||||
|
backup_file_path,
|
||||||
|
gzip=False,
|
||||||
|
mode="r",
|
||||||
|
) as ostf,
|
||||||
|
):
|
||||||
|
ostf.extractall(
|
||||||
|
path=Path(tempdir, "extracted"),
|
||||||
|
members=securetar.secure_path(ostf),
|
||||||
|
filter="fully_trusted",
|
||||||
|
)
|
||||||
|
backup_meta_file = Path(tempdir, "extracted", "backup.json")
|
||||||
|
backup_meta = json.loads(backup_meta_file.read_text(encoding="utf8"))
|
||||||
|
|
||||||
|
if (
|
||||||
|
backup_meta_version := AwesomeVersion(
|
||||||
|
backup_meta["homeassistant"]["version"]
|
||||||
|
)
|
||||||
|
) > HA_VERSION:
|
||||||
|
raise ValueError(
|
||||||
|
f"You need at least Home Assistant version {backup_meta_version} to restore this backup"
|
||||||
|
)
|
||||||
|
|
||||||
|
with securetar.SecureTarFile(
|
||||||
|
Path(
|
||||||
|
tempdir,
|
||||||
|
"extracted",
|
||||||
|
f"homeassistant.tar{'.gz' if backup_meta["compressed"] else ''}",
|
||||||
|
),
|
||||||
|
gzip=backup_meta["compressed"],
|
||||||
|
mode="r",
|
||||||
|
) as istf:
|
||||||
|
for member in istf.getmembers():
|
||||||
|
if member.name == "data":
|
||||||
|
continue
|
||||||
|
member.name = member.name.replace("data/", "")
|
||||||
|
_clear_configuration_directory(config_dir)
|
||||||
|
istf.extractall(
|
||||||
|
path=config_dir,
|
||||||
|
members=[
|
||||||
|
member
|
||||||
|
for member in securetar.secure_path(istf)
|
||||||
|
if member.name != "data"
|
||||||
|
],
|
||||||
|
filter="fully_trusted",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def restore_backup(config_dir_path: str) -> bool:
|
||||||
|
"""Restore the backup file if any.
|
||||||
|
|
||||||
|
Returns True if a restore backup file was found and restored, False otherwise.
|
||||||
|
"""
|
||||||
|
config_dir = Path(config_dir_path)
|
||||||
|
if not (restore_content := restore_backup_file_content(config_dir)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
||||||
|
backup_file_path = restore_content.backup_file_path
|
||||||
|
_LOGGER.info("Restoring %s", backup_file_path)
|
||||||
|
try:
|
||||||
|
_extract_backup(config_dir, backup_file_path)
|
||||||
|
except FileNotFoundError as err:
|
||||||
|
raise ValueError(f"Backup file {backup_file_path} does not exist") from err
|
||||||
|
_LOGGER.info("Restore complete, restarting")
|
||||||
|
return True
|
|
@ -70,6 +70,7 @@ from .const import (
|
||||||
REQUIRED_NEXT_PYTHON_VER,
|
REQUIRED_NEXT_PYTHON_VER,
|
||||||
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
||||||
)
|
)
|
||||||
|
from .core_config import async_process_ha_core_config
|
||||||
from .exceptions import HomeAssistantError
|
from .exceptions import HomeAssistantError
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
area_registry,
|
area_registry,
|
||||||
|
@ -479,7 +480,7 @@ async def async_from_config_dict(
|
||||||
core_config = config.get(core.DOMAIN, {})
|
core_config = config.get(core.DOMAIN, {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await conf_util.async_process_ha_core_config(hass, core_config)
|
await async_process_ha_core_config(hass, core_config)
|
||||||
except vol.Invalid as config_err:
|
except vol.Invalid as config_err:
|
||||||
conf_util.async_log_schema_error(config_err, core.DOMAIN, core_config, hass)
|
conf_util.async_log_schema_error(config_err, core.DOMAIN, core_config, hass)
|
||||||
async_notify_setup_error(hass, core.DOMAIN)
|
async_notify_setup_error(hass, core.DOMAIN)
|
||||||
|
@ -514,7 +515,7 @@ async def async_from_config_dict(
|
||||||
issue_registry.async_create_issue(
|
issue_registry.async_create_issue(
|
||||||
hass,
|
hass,
|
||||||
core.DOMAIN,
|
core.DOMAIN,
|
||||||
"python_version",
|
f"python_version_{required_python_version}",
|
||||||
is_fixable=False,
|
is_fixable=False,
|
||||||
severity=issue_registry.IssueSeverity.WARNING,
|
severity=issue_registry.IssueSeverity.WARNING,
|
||||||
breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
||||||
|
|
5
homeassistant/brands/husqvarna.json
Normal file
5
homeassistant/brands/husqvarna.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"domain": "husqvarna",
|
||||||
|
"name": "Husqvarna",
|
||||||
|
"integrations": ["husqvarna_automower", "husqvarna_automower_ble"]
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"domain": "lg",
|
"domain": "lg",
|
||||||
"name": "LG",
|
"name": "LG",
|
||||||
"integrations": ["lg_netcast", "lg_soundbar", "webostv"]
|
"integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
|
||||||
}
|
}
|
||||||
|
|
5
homeassistant/brands/sky.json
Normal file
5
homeassistant/brands/sky.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"domain": "sky",
|
||||||
|
"name": "Sky",
|
||||||
|
"integrations": ["sky_hub", "sky_remote"]
|
||||||
|
}
|
|
@ -7,13 +7,9 @@ from jaraco.abode.devices.alarm import Alarm
|
||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
AlarmControlPanelEntity,
|
AlarmControlPanelEntity,
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
|
AlarmControlPanelState,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
@ -44,14 +40,14 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanelEntity):
|
||||||
_device: Alarm
|
_device: Alarm
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str | None:
|
def alarm_state(self) -> AlarmControlPanelState | None:
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
if self._device.is_standby:
|
if self._device.is_standby:
|
||||||
return STATE_ALARM_DISARMED
|
return AlarmControlPanelState.DISARMED
|
||||||
if self._device.is_away:
|
if self._device.is_away:
|
||||||
return STATE_ALARM_ARMED_AWAY
|
return AlarmControlPanelState.ARMED_AWAY
|
||||||
if self._device.is_home:
|
if self._device.is_home:
|
||||||
return STATE_ALARM_ARMED_HOME
|
return AlarmControlPanelState.ARMED_HOME
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def alarm_disarm(self, code: str | None = None) -> None:
|
def alarm_disarm(self, code: str | None = None) -> None:
|
||||||
|
|
29
homeassistant/components/acaia/__init__.py
Normal file
29
homeassistant/components/acaia/__init__.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
"""Initialize the Acaia component."""
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import AcaiaConfigEntry, AcaiaCoordinator
|
||||||
|
|
||||||
|
PLATFORMS = [
|
||||||
|
Platform.BUTTON,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: AcaiaConfigEntry) -> bool:
|
||||||
|
"""Set up acaia as config entry."""
|
||||||
|
|
||||||
|
coordinator = AcaiaCoordinator(hass, entry)
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: AcaiaConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
61
homeassistant/components/acaia/button.py
Normal file
61
homeassistant/components/acaia/button.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
"""Button entities for Acaia scales."""
|
||||||
|
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aioacaia.acaiascale import AcaiaScale
|
||||||
|
|
||||||
|
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import AcaiaConfigEntry
|
||||||
|
from .entity import AcaiaEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True, frozen=True)
|
||||||
|
class AcaiaButtonEntityDescription(ButtonEntityDescription):
|
||||||
|
"""Description for acaia button entities."""
|
||||||
|
|
||||||
|
press_fn: Callable[[AcaiaScale], Coroutine[Any, Any, None]]
|
||||||
|
|
||||||
|
|
||||||
|
BUTTONS: tuple[AcaiaButtonEntityDescription, ...] = (
|
||||||
|
AcaiaButtonEntityDescription(
|
||||||
|
key="tare",
|
||||||
|
translation_key="tare",
|
||||||
|
press_fn=lambda scale: scale.tare(),
|
||||||
|
),
|
||||||
|
AcaiaButtonEntityDescription(
|
||||||
|
key="reset_timer",
|
||||||
|
translation_key="reset_timer",
|
||||||
|
press_fn=lambda scale: scale.reset_timer(),
|
||||||
|
),
|
||||||
|
AcaiaButtonEntityDescription(
|
||||||
|
key="start_stop",
|
||||||
|
translation_key="start_stop",
|
||||||
|
press_fn=lambda scale: scale.start_stop_timer(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AcaiaConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up button entities and services."""
|
||||||
|
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
async_add_entities(AcaiaButton(coordinator, description) for description in BUTTONS)
|
||||||
|
|
||||||
|
|
||||||
|
class AcaiaButton(AcaiaEntity, ButtonEntity):
|
||||||
|
"""Representation of an Acaia button."""
|
||||||
|
|
||||||
|
entity_description: AcaiaButtonEntityDescription
|
||||||
|
|
||||||
|
async def async_press(self) -> None:
|
||||||
|
"""Handle the button press."""
|
||||||
|
await self.entity_description.press_fn(self._scale)
|
149
homeassistant/components/acaia/config_flow.py
Normal file
149
homeassistant/components/acaia/config_flow.py
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
"""Config flow for Acaia integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError, AcaiaUnknownDevice
|
||||||
|
from aioacaia.helpers import is_new_scale
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.bluetooth import (
|
||||||
|
BluetoothServiceInfoBleak,
|
||||||
|
async_discovered_service_info,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_ADDRESS, CONF_NAME
|
||||||
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
SelectOptionDict,
|
||||||
|
SelectSelector,
|
||||||
|
SelectSelectorConfig,
|
||||||
|
SelectSelectorMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import CONF_IS_NEW_STYLE_SCALE, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for acaia."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the config flow."""
|
||||||
|
self._discovered: dict[str, Any] = {}
|
||||||
|
self._discovered_devices: dict[str, str] = {}
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
mac = format_mac(user_input[CONF_ADDRESS])
|
||||||
|
try:
|
||||||
|
is_new_style_scale = await is_new_scale(mac)
|
||||||
|
except AcaiaDeviceNotFound:
|
||||||
|
errors["base"] = "device_not_found"
|
||||||
|
except AcaiaError:
|
||||||
|
_LOGGER.exception("Error occurred while connecting to the scale")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
except AcaiaUnknownDevice:
|
||||||
|
return self.async_abort(reason="unsupported_device")
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(mac)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self._discovered_devices[user_input[CONF_ADDRESS]],
|
||||||
|
data={
|
||||||
|
CONF_ADDRESS: mac,
|
||||||
|
CONF_IS_NEW_STYLE_SCALE: is_new_style_scale,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for device in async_discovered_service_info(self.hass):
|
||||||
|
self._discovered_devices[device.address] = device.name
|
||||||
|
|
||||||
|
if not self._discovered_devices:
|
||||||
|
return self.async_abort(reason="no_devices_found")
|
||||||
|
|
||||||
|
options = [
|
||||||
|
SelectOptionDict(
|
||||||
|
value=device_mac,
|
||||||
|
label=f"{device_name} ({device_mac})",
|
||||||
|
)
|
||||||
|
for device_mac, device_name in self._discovered_devices.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_ADDRESS): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=options,
|
||||||
|
mode=SelectSelectorMode.DROPDOWN,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_bluetooth(
|
||||||
|
self, discovery_info: BluetoothServiceInfoBleak
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a discovered Bluetooth device."""
|
||||||
|
|
||||||
|
self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address)
|
||||||
|
self._discovered[CONF_NAME] = discovery_info.name
|
||||||
|
|
||||||
|
await self.async_set_unique_id(mac)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._discovered[CONF_IS_NEW_STYLE_SCALE] = await is_new_scale(
|
||||||
|
discovery_info.address
|
||||||
|
)
|
||||||
|
except AcaiaDeviceNotFound:
|
||||||
|
_LOGGER.debug("Device not found during discovery")
|
||||||
|
return self.async_abort(reason="device_not_found")
|
||||||
|
except AcaiaError:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Error occurred while connecting to the scale during discovery",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return self.async_abort(reason="unknown")
|
||||||
|
except AcaiaUnknownDevice:
|
||||||
|
_LOGGER.debug("Unsupported device during discovery")
|
||||||
|
return self.async_abort(reason="unsupported_device")
|
||||||
|
|
||||||
|
return await self.async_step_bluetooth_confirm()
|
||||||
|
|
||||||
|
async def async_step_bluetooth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle confirmation of Bluetooth discovery."""
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self._discovered[CONF_NAME],
|
||||||
|
data={
|
||||||
|
CONF_ADDRESS: self._discovered[CONF_ADDRESS],
|
||||||
|
CONF_IS_NEW_STYLE_SCALE: self._discovered[CONF_IS_NEW_STYLE_SCALE],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.context["title_placeholders"] = placeholders = {
|
||||||
|
CONF_NAME: self._discovered[CONF_NAME]
|
||||||
|
}
|
||||||
|
|
||||||
|
self._set_confirm_only()
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="bluetooth_confirm",
|
||||||
|
description_placeholders=placeholders,
|
||||||
|
)
|
4
homeassistant/components/acaia/const.py
Normal file
4
homeassistant/components/acaia/const.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
"""Constants for component."""
|
||||||
|
|
||||||
|
DOMAIN = "acaia"
|
||||||
|
CONF_IS_NEW_STYLE_SCALE = "is_new_style_scale"
|
86
homeassistant/components/acaia/coordinator.py
Normal file
86
homeassistant/components/acaia/coordinator.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
"""Coordinator for Acaia integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aioacaia.acaiascale import AcaiaScale
|
||||||
|
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_ADDRESS
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
|
from .const import CONF_IS_NEW_STYLE_SCALE
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type AcaiaConfigEntry = ConfigEntry[AcaiaCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class AcaiaCoordinator(DataUpdateCoordinator[None]):
|
||||||
|
"""Class to handle fetching data from the scale."""
|
||||||
|
|
||||||
|
config_entry: AcaiaConfigEntry
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entry: AcaiaConfigEntry) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name="acaia coordinator",
|
||||||
|
update_interval=SCAN_INTERVAL,
|
||||||
|
config_entry=entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._scale = AcaiaScale(
|
||||||
|
address_or_ble_device=entry.data[CONF_ADDRESS],
|
||||||
|
name=entry.title,
|
||||||
|
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
|
||||||
|
notify_callback=self.async_update_listeners,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scale(self) -> AcaiaScale:
|
||||||
|
"""Return the scale object."""
|
||||||
|
return self._scale
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> None:
|
||||||
|
"""Fetch data."""
|
||||||
|
|
||||||
|
# scale is already connected, return
|
||||||
|
if self._scale.connected:
|
||||||
|
return
|
||||||
|
|
||||||
|
# scale is not connected, try to connect
|
||||||
|
try:
|
||||||
|
await self._scale.connect(setup_tasks=False)
|
||||||
|
except (AcaiaDeviceNotFound, AcaiaError, TimeoutError) as ex:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Could not connect to scale: %s, Error: %s",
|
||||||
|
self.config_entry.data[CONF_ADDRESS],
|
||||||
|
ex,
|
||||||
|
)
|
||||||
|
self._scale.device_disconnected_handler(notify=False)
|
||||||
|
return
|
||||||
|
|
||||||
|
# connected, set up background tasks
|
||||||
|
if not self._scale.heartbeat_task or self._scale.heartbeat_task.done():
|
||||||
|
self._scale.heartbeat_task = self.config_entry.async_create_background_task(
|
||||||
|
hass=self.hass,
|
||||||
|
target=self._scale.send_heartbeats(),
|
||||||
|
name="acaia_heartbeat_task",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self._scale.process_queue_task or self._scale.process_queue_task.done():
|
||||||
|
self._scale.process_queue_task = (
|
||||||
|
self.config_entry.async_create_background_task(
|
||||||
|
hass=self.hass,
|
||||||
|
target=self._scale.process_queue(),
|
||||||
|
name="acaia_process_queue_task",
|
||||||
|
)
|
||||||
|
)
|
40
homeassistant/components/acaia/entity.py
Normal file
40
homeassistant/components/acaia/entity.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
"""Base class for Acaia entities."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AcaiaCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]):
|
||||||
|
"""Common elements for all entities."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AcaiaCoordinator,
|
||||||
|
entity_description: EntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = entity_description
|
||||||
|
self._scale = coordinator.scale
|
||||||
|
self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}"
|
||||||
|
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, self._scale.mac)},
|
||||||
|
manufacturer="Acaia",
|
||||||
|
model=self._scale.model,
|
||||||
|
suggested_area="Kitchen",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Returns whether entity is available."""
|
||||||
|
return super().available and self._scale.connected
|
15
homeassistant/components/acaia/icons.json
Normal file
15
homeassistant/components/acaia/icons.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"button": {
|
||||||
|
"tare": {
|
||||||
|
"default": "mdi:scale-balance"
|
||||||
|
},
|
||||||
|
"reset_timer": {
|
||||||
|
"default": "mdi:timer-refresh"
|
||||||
|
},
|
||||||
|
"start_stop": {
|
||||||
|
"default": "mdi:timer-play"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
homeassistant/components/acaia/manifest.json
Normal file
29
homeassistant/components/acaia/manifest.json
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"domain": "acaia",
|
||||||
|
"name": "Acaia",
|
||||||
|
"bluetooth": [
|
||||||
|
{
|
||||||
|
"manufacturer_id": 16962
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"local_name": "ACAIA*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"local_name": "PYXIS-*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"local_name": "LUNAR-*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"local_name": "PROCHBT001"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codeowners": ["@zweckj"],
|
||||||
|
"config_flow": true,
|
||||||
|
"dependencies": ["bluetooth_adapters"],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/acaia",
|
||||||
|
"integration_type": "device",
|
||||||
|
"iot_class": "local_push",
|
||||||
|
"loggers": ["aioacaia"],
|
||||||
|
"requirements": ["aioacaia==0.1.6"]
|
||||||
|
}
|
38
homeassistant/components/acaia/strings.json
Normal file
38
homeassistant/components/acaia/strings.json
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "{name}",
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||||
|
"unsupported_device": "This device is not supported."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"device_not_found": "Device could not be found.",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"bluetooth_confirm": {
|
||||||
|
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"description": "[%key:component::bluetooth::config::step::user::description%]",
|
||||||
|
"data": {
|
||||||
|
"address": "[%key:common::config_flow::data::device%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"button": {
|
||||||
|
"tare": {
|
||||||
|
"name": "Tare"
|
||||||
|
},
|
||||||
|
"reset_timer": {
|
||||||
|
"name": "Reset timer"
|
||||||
|
},
|
||||||
|
"start_stop": {
|
||||||
|
"name": "Start/stop timer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ from typing import Any
|
||||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.hassio import HassioServiceInfo
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
|
@ -18,6 +17,7 @@ from homeassistant.const import (
|
||||||
CONF_VERIFY_SSL,
|
CONF_VERIFY_SSL,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ async def async_setup_entry(
|
||||||
coordinator = DataUpdateCoordinator(
|
coordinator = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
name="Advantage Air",
|
name="Advantage Air",
|
||||||
update_method=async_get,
|
update_method=async_get,
|
||||||
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
|
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
"""The AEMET OpenData component."""
|
"""The AEMET OpenData component."""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aemet_opendata.exceptions import AemetError, TownNotFound
|
from aemet_opendata.exceptions import AemetError, TownNotFound
|
||||||
|
@ -13,20 +12,10 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
from .const import CONF_STATION_UPDATES, PLATFORMS
|
from .const import CONF_STATION_UPDATES, PLATFORMS
|
||||||
from .coordinator import WeatherUpdateCoordinator
|
from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
type AemetConfigEntry = ConfigEntry[AemetData]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AemetData:
|
|
||||||
"""Aemet runtime data."""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
coordinator: WeatherUpdateCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> bool:
|
||||||
"""Set up AEMET OpenData as config entry."""
|
"""Set up AEMET OpenData as config entry."""
|
||||||
|
@ -46,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
|
||||||
except AemetError as err:
|
except AemetError as err:
|
||||||
raise ConfigEntryNotReady(err) from err
|
raise ConfigEntryNotReady(err) from err
|
||||||
|
|
||||||
weather_coordinator = WeatherUpdateCoordinator(hass, aemet)
|
weather_coordinator = WeatherUpdateCoordinator(hass, entry, aemet)
|
||||||
await weather_coordinator.async_config_entry_first_refresh()
|
await weather_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
entry.runtime_data = AemetData(name=name, coordinator=weather_coordinator)
|
entry.runtime_data = AemetData(name=name, coordinator=weather_coordinator)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from asyncio import timeout
|
from asyncio import timeout
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Final, cast
|
from typing import Any, Final, cast
|
||||||
|
@ -19,6 +20,7 @@ from aemet_opendata.helpers import dict_nested_value
|
||||||
from aemet_opendata.interface import AEMET
|
from aemet_opendata.interface import AEMET
|
||||||
|
|
||||||
from homeassistant.components.weather import Forecast
|
from homeassistant.components.weather import Forecast
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
@ -29,6 +31,16 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
API_TIMEOUT: Final[int] = 120
|
API_TIMEOUT: Final[int] = 120
|
||||||
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
|
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
|
||||||
|
|
||||||
|
type AemetConfigEntry = ConfigEntry[AemetData]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AemetData:
|
||||||
|
"""Aemet runtime data."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
coordinator: WeatherUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""Weather data update coordinator."""
|
"""Weather data update coordinator."""
|
||||||
|
@ -36,6 +48,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
entry: AemetConfigEntry,
|
||||||
aemet: AEMET,
|
aemet: AEMET,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize coordinator."""
|
"""Initialize coordinator."""
|
||||||
|
@ -44,6 +57,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=WEATHER_UPDATE_INTERVAL,
|
update_interval=WEATHER_UPDATE_INTERVAL,
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import AemetConfigEntry
|
from .coordinator import AemetConfigEntry
|
||||||
|
|
||||||
TO_REDACT_CONFIG = [
|
TO_REDACT_CONFIG = [
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
|
|
|
@ -55,7 +55,6 @@ from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import AemetConfigEntry
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_API_CONDITION,
|
ATTR_API_CONDITION,
|
||||||
ATTR_API_FORECAST_CONDITION,
|
ATTR_API_FORECAST_CONDITION,
|
||||||
|
@ -87,7 +86,7 @@ from .const import (
|
||||||
ATTR_API_WIND_SPEED,
|
ATTR_API_WIND_SPEED,
|
||||||
CONDITIONS_MAP,
|
CONDITIONS_MAP,
|
||||||
)
|
)
|
||||||
from .coordinator import WeatherUpdateCoordinator
|
from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator
|
||||||
from .entity import AemetEntity
|
from .entity import AemetEntity
|
||||||
|
|
||||||
|
|
||||||
|
@ -249,6 +248,7 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||||
name="Rain",
|
name="Rain",
|
||||||
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
||||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
AemetSensorEntityDescription(
|
AemetSensorEntityDescription(
|
||||||
key=ATTR_API_RAIN_PROB,
|
key=ATTR_API_RAIN_PROB,
|
||||||
|
@ -263,6 +263,7 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||||
name="Snow",
|
name="Snow",
|
||||||
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
||||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
AemetSensorEntityDescription(
|
AemetSensorEntityDescription(
|
||||||
key=ATTR_API_SNOW_PROB,
|
key=ATTR_API_SNOW_PROB,
|
||||||
|
|
|
@ -27,9 +27,8 @@ from homeassistant.const import (
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AemetConfigEntry
|
|
||||||
from .const import CONDITIONS_MAP
|
from .const import CONDITIONS_MAP
|
||||||
from .coordinator import WeatherUpdateCoordinator
|
from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator
|
||||||
from .entity import AemetEntity
|
from .entity import AemetEntity
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,7 @@ from __future__ import annotations
|
||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
AlarmControlPanelEntity,
|
AlarmControlPanelEntity,
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
)
|
AlarmControlPanelState,
|
||||||
from homeassistant.const import (
|
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
@ -65,37 +60,37 @@ class AgentBaseStation(AlarmControlPanelEntity):
|
||||||
self._attr_available = self._client.is_available
|
self._attr_available = self._client.is_available
|
||||||
armed = self._client.is_armed
|
armed = self._client.is_armed
|
||||||
if armed is None:
|
if armed is None:
|
||||||
self._attr_state = None
|
self._attr_alarm_state = None
|
||||||
return
|
return
|
||||||
if armed:
|
if armed:
|
||||||
prof = (await self._client.get_active_profile()).lower()
|
prof = (await self._client.get_active_profile()).lower()
|
||||||
self._attr_state = STATE_ALARM_ARMED_AWAY
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
|
||||||
if prof == CONF_HOME_MODE_NAME:
|
if prof == CONF_HOME_MODE_NAME:
|
||||||
self._attr_state = STATE_ALARM_ARMED_HOME
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
|
||||||
elif prof == CONF_NIGHT_MODE_NAME:
|
elif prof == CONF_NIGHT_MODE_NAME:
|
||||||
self._attr_state = STATE_ALARM_ARMED_NIGHT
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
|
||||||
else:
|
else:
|
||||||
self._attr_state = STATE_ALARM_DISARMED
|
self._attr_alarm_state = AlarmControlPanelState.DISARMED
|
||||||
|
|
||||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
await self._client.disarm()
|
await self._client.disarm()
|
||||||
self._attr_state = STATE_ALARM_DISARMED
|
self._attr_alarm_state = AlarmControlPanelState.DISARMED
|
||||||
|
|
||||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||||
"""Send arm away command. Uses custom mode."""
|
"""Send arm away command. Uses custom mode."""
|
||||||
await self._client.arm()
|
await self._client.arm()
|
||||||
await self._client.set_active_profile(CONF_AWAY_MODE_NAME)
|
await self._client.set_active_profile(CONF_AWAY_MODE_NAME)
|
||||||
self._attr_state = STATE_ALARM_ARMED_AWAY
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
|
||||||
|
|
||||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||||
"""Send arm home command. Uses custom mode."""
|
"""Send arm home command. Uses custom mode."""
|
||||||
await self._client.arm()
|
await self._client.arm()
|
||||||
await self._client.set_active_profile(CONF_HOME_MODE_NAME)
|
await self._client.set_active_profile(CONF_HOME_MODE_NAME)
|
||||||
self._attr_state = STATE_ALARM_ARMED_HOME
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
|
||||||
|
|
||||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||||
"""Send arm night command. Uses custom mode."""
|
"""Send arm night command. Uses custom mode."""
|
||||||
await self._client.arm()
|
await self._client.arm()
|
||||||
await self._client.set_active_profile(CONF_NIGHT_MODE_NAME)
|
await self._client.set_active_profile(CONF_NIGHT_MODE_NAME)
|
||||||
self._attr_state = STATE_ALARM_ARMED_NIGHT
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
|
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["agent"],
|
"loggers": ["agent"],
|
||||||
"requirements": ["agent-py==0.0.23"]
|
"requirements": ["agent-py==0.0.24"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
"""Config flow for AirNow integration."""
|
"""Config flow for AirNow integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@ -12,7 +14,6 @@ from homeassistant.config_entries import (
|
||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
OptionsFlow,
|
OptionsFlow,
|
||||||
OptionsFlowWithConfigEntry,
|
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
@ -120,12 +121,12 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
) -> OptionsFlow:
|
) -> AirNowOptionsFlowHandler:
|
||||||
"""Return the options flow."""
|
"""Return the options flow."""
|
||||||
return AirNowOptionsFlowHandler(config_entry)
|
return AirNowOptionsFlowHandler()
|
||||||
|
|
||||||
|
|
||||||
class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
class AirNowOptionsFlowHandler(OptionsFlow):
|
||||||
"""Handle an options flow for AirNow."""
|
"""Handle an options flow for AirNow."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
|
@ -136,12 +137,7 @@ class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||||
return self.async_create_entry(data=user_input)
|
return self.async_create_entry(data=user_input)
|
||||||
|
|
||||||
options_schema = vol.Schema(
|
options_schema = vol.Schema(
|
||||||
{
|
{vol.Optional(CONF_RADIUS): vol.All(int, vol.Range(min=5))}
|
||||||
vol.Optional(CONF_RADIUS): vol.All(
|
|
||||||
int,
|
|
||||||
vol.Range(min=5),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
|
|
|
@ -42,6 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
||||||
coordinator = DataUpdateCoordinator(
|
coordinator = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_method=_update_method,
|
update_method=_update_method,
|
||||||
update_interval=SCAN_INTERVAL,
|
update_interval=SCAN_INTERVAL,
|
||||||
|
|
|
@ -2,75 +2,27 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
|
|
||||||
from bleak_retry_connector import close_stale_connections_by_address
|
|
||||||
|
|
||||||
from homeassistant.components import bluetooth
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
||||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
|
||||||
|
|
||||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MAX_RETRIES_AFTER_STARTUP
|
from .const import MAX_RETRIES_AFTER_STARTUP
|
||||||
|
from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator[AirthingsDevice]
|
|
||||||
AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: AirthingsBLEConfigEntry
|
hass: HomeAssistant, entry: AirthingsBLEConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set up Airthings BLE device from a config entry."""
|
"""Set up Airthings BLE device from a config entry."""
|
||||||
hass.data.setdefault(DOMAIN, {})
|
coordinator = AirthingsBLEDataUpdateCoordinator(hass, entry)
|
||||||
address = entry.unique_id
|
|
||||||
|
|
||||||
is_metric = hass.config.units is METRIC_SYSTEM
|
|
||||||
assert address is not None
|
|
||||||
|
|
||||||
await close_stale_connections_by_address(address)
|
|
||||||
|
|
||||||
ble_device = bluetooth.async_ble_device_from_address(hass, address)
|
|
||||||
|
|
||||||
if not ble_device:
|
|
||||||
raise ConfigEntryNotReady(
|
|
||||||
f"Could not find Airthings device with address {address}"
|
|
||||||
)
|
|
||||||
|
|
||||||
airthings = AirthingsBluetoothDeviceData(_LOGGER, is_metric)
|
|
||||||
|
|
||||||
async def _async_update_method() -> AirthingsDevice:
|
|
||||||
"""Get data from Airthings BLE."""
|
|
||||||
try:
|
|
||||||
data = await airthings.update_device(ble_device)
|
|
||||||
except Exception as err:
|
|
||||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
coordinator: AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
name=DOMAIN,
|
|
||||||
update_method=_async_update_method,
|
|
||||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
|
||||||
)
|
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
# Once its setup and we know we are not going to delay
|
# Once its setup and we know we are not going to delay
|
||||||
# the startup of Home Assistant, we can set the max attempts
|
# the startup of Home Assistant, we can set the max attempts
|
||||||
# to a higher value. If the first connection attempt fails,
|
# to a higher value. If the first connection attempt fails,
|
||||||
# Home Assistant's built-in retry logic will take over.
|
# Home Assistant's built-in retry logic will take over.
|
||||||
airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP)
|
coordinator.airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP)
|
||||||
|
|
||||||
entry.runtime_data = coordinator
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
|
68
homeassistant/components/airthings_ble/coordinator.py
Normal file
68
homeassistant/components/airthings_ble/coordinator.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
"""The Airthings BLE integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
|
||||||
|
from bleak.backends.device import BLEDevice
|
||||||
|
from bleak_retry_connector import close_stale_connections_by_address
|
||||||
|
|
||||||
|
from homeassistant.components import bluetooth
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||||
|
|
||||||
|
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
|
||||||
|
"""Class to manage fetching Airthings BLE data."""
|
||||||
|
|
||||||
|
ble_device: BLEDevice
|
||||||
|
config_entry: AirthingsBLEConfigEntry
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entry: AirthingsBLEConfigEntry) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
self.airthings = AirthingsBluetoothDeviceData(
|
||||||
|
_LOGGER, hass.config.units is METRIC_SYSTEM
|
||||||
|
)
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_setup(self) -> None:
|
||||||
|
"""Set up the coordinator."""
|
||||||
|
address = self.config_entry.unique_id
|
||||||
|
|
||||||
|
assert address is not None
|
||||||
|
|
||||||
|
await close_stale_connections_by_address(address)
|
||||||
|
|
||||||
|
ble_device = bluetooth.async_ble_device_from_address(self.hass, address)
|
||||||
|
|
||||||
|
if not ble_device:
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
f"Could not find Airthings device with address {address}"
|
||||||
|
)
|
||||||
|
self.ble_device = ble_device
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> AirthingsDevice:
|
||||||
|
"""Get data from Airthings BLE."""
|
||||||
|
try:
|
||||||
|
data = await self.airthings.update_device(self.ble_device)
|
||||||
|
except Exception as err:
|
||||||
|
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||||
|
|
||||||
|
return data
|
|
@ -24,5 +24,5 @@
|
||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["airthings-ble==0.9.1"]
|
"requirements": ["airthings-ble==0.9.2"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,8 @@ from homeassistant.helpers.typing import StateType
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||||
|
|
||||||
from . import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
|
|
||||||
from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE
|
from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE
|
||||||
|
from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,6 @@ from homeassistant.const import CONF_HOST, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER]
|
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER]
|
||||||
|
|
||||||
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
|
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
|
||||||
|
@ -19,8 +17,6 @@ type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: Airtouch5ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: Airtouch5ConfigEntry) -> bool:
|
||||||
"""Set up Airtouch 5 from a config entry."""
|
"""Set up Airtouch 5 from a config entry."""
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
|
||||||
|
|
||||||
# Create API instance
|
# Create API instance
|
||||||
host = entry.data[CONF_HOST]
|
host = entry.data[CONF_HOST]
|
||||||
client = Airtouch5SimpleClient(host)
|
client = Airtouch5SimpleClient(host)
|
||||||
|
|
|
@ -204,6 +204,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
|
||||||
coordinator = DataUpdateCoordinator(
|
coordinator = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
name=async_get_geography_id(entry.data),
|
name=async_get_geography_id(entry.data),
|
||||||
# We give a placeholder update interval in order to create the coordinator;
|
# We give a placeholder update interval in order to create the coordinator;
|
||||||
# then, below, we use the coordinator's presence (along with any other
|
# then, below, we use the coordinator's presence (along with any other
|
||||||
|
|
|
@ -81,6 +81,7 @@ async def async_setup_entry(
|
||||||
coordinator = DataUpdateCoordinator(
|
coordinator = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
name="Node/Pro data",
|
name="Node/Pro data",
|
||||||
update_interval=UPDATE_INTERVAL,
|
update_interval=UPDATE_INTERVAL,
|
||||||
update_method=async_get_data,
|
update_method=async_get_data,
|
||||||
|
|
|
@ -11,5 +11,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairzone"],
|
"loggers": ["aioairzone"],
|
||||||
"requirements": ["aioairzone==0.9.5"]
|
"requirements": ["aioairzone==0.9.6"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -310,6 +310,10 @@ class AirzoneDeviceClimate(AirzoneClimate):
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
|
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
||||||
|
if hvac_mode is not None:
|
||||||
|
await self.async_set_hvac_mode(hvac_mode)
|
||||||
|
|
||||||
params: dict[str, Any] = {}
|
params: dict[str, Any] = {}
|
||||||
if ATTR_TEMPERATURE in kwargs:
|
if ATTR_TEMPERATURE in kwargs:
|
||||||
params[API_SETPOINT] = {
|
params[API_SETPOINT] = {
|
||||||
|
@ -333,9 +337,6 @@ class AirzoneDeviceClimate(AirzoneClimate):
|
||||||
}
|
}
|
||||||
await self._async_update_params(params)
|
await self._async_update_params(params)
|
||||||
|
|
||||||
if ATTR_HVAC_MODE in kwargs:
|
|
||||||
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
|
|
||||||
|
|
||||||
|
|
||||||
class AirzoneDeviceGroupClimate(AirzoneClimate):
|
class AirzoneDeviceGroupClimate(AirzoneClimate):
|
||||||
"""Define an Airzone Cloud DeviceGroup base class."""
|
"""Define an Airzone Cloud DeviceGroup base class."""
|
||||||
|
@ -366,6 +367,10 @@ class AirzoneDeviceGroupClimate(AirzoneClimate):
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
|
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
||||||
|
if hvac_mode is not None:
|
||||||
|
await self.async_set_hvac_mode(hvac_mode)
|
||||||
|
|
||||||
params: dict[str, Any] = {}
|
params: dict[str, Any] = {}
|
||||||
if ATTR_TEMPERATURE in kwargs:
|
if ATTR_TEMPERATURE in kwargs:
|
||||||
params[API_PARAMS] = {
|
params[API_PARAMS] = {
|
||||||
|
@ -376,9 +381,6 @@ class AirzoneDeviceGroupClimate(AirzoneClimate):
|
||||||
}
|
}
|
||||||
await self._async_update_params(params)
|
await self._async_update_params(params)
|
||||||
|
|
||||||
if ATTR_HVAC_MODE in kwargs:
|
|
||||||
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
|
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
"""Set hvac mode."""
|
"""Set hvac mode."""
|
||||||
params: dict[str, Any] = {
|
params: dict[str, Any] = {
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioairzone_cloud"],
|
"loggers": ["aioairzone_cloud"],
|
||||||
"requirements": ["aioairzone-cloud==0.6.7"]
|
"requirements": ["aioairzone-cloud==0.6.10"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Final, final
|
from typing import TYPE_CHECKING, Any, Final, final
|
||||||
|
|
||||||
from propcache import cached_property
|
from propcache import cached_property
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -33,6 +34,7 @@ from homeassistant.helpers.deprecation import (
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
|
@ -49,6 +51,7 @@ from .const import ( # noqa: F401
|
||||||
ATTR_CODE_ARM_REQUIRED,
|
ATTR_CODE_ARM_REQUIRED,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
|
AlarmControlPanelState,
|
||||||
CodeFormat,
|
CodeFormat,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -142,6 +145,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
||||||
"changed_by",
|
"changed_by",
|
||||||
"code_arm_required",
|
"code_arm_required",
|
||||||
"supported_features",
|
"supported_features",
|
||||||
|
"alarm_state",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,6 +153,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
||||||
"""An abstract class for alarm control entities."""
|
"""An abstract class for alarm control entities."""
|
||||||
|
|
||||||
entity_description: AlarmControlPanelEntityDescription
|
entity_description: AlarmControlPanelEntityDescription
|
||||||
|
_attr_alarm_state: AlarmControlPanelState | None = None
|
||||||
_attr_changed_by: str | None = None
|
_attr_changed_by: str | None = None
|
||||||
_attr_code_arm_required: bool = True
|
_attr_code_arm_required: bool = True
|
||||||
_attr_code_format: CodeFormat | None = None
|
_attr_code_format: CodeFormat | None = None
|
||||||
|
@ -157,6 +162,84 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
||||||
)
|
)
|
||||||
_alarm_control_panel_option_default_code: str | None = None
|
_alarm_control_panel_option_default_code: str | None = None
|
||||||
|
|
||||||
|
__alarm_legacy_state: bool = False
|
||||||
|
__alarm_legacy_state_reported: bool = False
|
||||||
|
|
||||||
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||||
|
"""Post initialisation processing."""
|
||||||
|
super().__init_subclass__(**kwargs)
|
||||||
|
if any(method in cls.__dict__ for method in ("_attr_state", "state")):
|
||||||
|
# Integrations should use the 'alarm_state' property instead of
|
||||||
|
# setting the state directly.
|
||||||
|
cls.__alarm_legacy_state = True
|
||||||
|
|
||||||
|
def __setattr__(self, __name: str, __value: Any) -> None:
|
||||||
|
"""Set attribute.
|
||||||
|
|
||||||
|
Deprecation warning if setting '_attr_state' directly
|
||||||
|
unless already reported.
|
||||||
|
"""
|
||||||
|
if __name == "_attr_state":
|
||||||
|
if self.__alarm_legacy_state_reported is not True:
|
||||||
|
self._report_deprecated_alarm_state_handling()
|
||||||
|
self.__alarm_legacy_state_reported = True
|
||||||
|
return super().__setattr__(__name, __value)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def add_to_platform_start(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
platform: EntityPlatform,
|
||||||
|
parallel_updates: asyncio.Semaphore | None,
|
||||||
|
) -> None:
|
||||||
|
"""Start adding an entity to a platform."""
|
||||||
|
super().add_to_platform_start(hass, platform, parallel_updates)
|
||||||
|
if self.__alarm_legacy_state and not self.__alarm_legacy_state_reported:
|
||||||
|
self._report_deprecated_alarm_state_handling()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _report_deprecated_alarm_state_handling(self) -> None:
|
||||||
|
"""Report on deprecated handling of alarm state.
|
||||||
|
|
||||||
|
Integrations should implement alarm_state instead of using state directly.
|
||||||
|
"""
|
||||||
|
self.__alarm_legacy_state_reported = True
|
||||||
|
if "custom_components" in type(self).__module__:
|
||||||
|
# Do not report on core integrations as they have been fixed.
|
||||||
|
report_issue = "report it to the custom integration author."
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Entity %s (%s) is setting state directly"
|
||||||
|
" which will stop working in HA Core 2025.11."
|
||||||
|
" Entities should implement the 'alarm_state' property and"
|
||||||
|
" return its state using the AlarmControlPanelState enum, please %s",
|
||||||
|
self.entity_id,
|
||||||
|
type(self),
|
||||||
|
report_issue,
|
||||||
|
)
|
||||||
|
|
||||||
|
@final
|
||||||
|
@property
|
||||||
|
def state(self) -> str | None:
|
||||||
|
"""Return the current state."""
|
||||||
|
if (alarm_state := self.alarm_state) is not None:
|
||||||
|
return alarm_state
|
||||||
|
if self._attr_state is not None:
|
||||||
|
# Backwards compatibility for integrations that set state directly
|
||||||
|
# Should be removed in 2025.11
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert isinstance(self._attr_state, str)
|
||||||
|
return self._attr_state
|
||||||
|
return None
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def alarm_state(self) -> AlarmControlPanelState | None:
|
||||||
|
"""Return the current alarm control panel entity state.
|
||||||
|
|
||||||
|
Integrations should overwrite this or use the '_attr_alarm_state'
|
||||||
|
attribute to set the alarm status using the 'AlarmControlPanelState' enum.
|
||||||
|
"""
|
||||||
|
return self._attr_alarm_state
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@callback
|
@callback
|
||||||
def code_or_default_code(self, code: str | None) -> str | None:
|
def code_or_default_code(self, code: str | None) -> str | None:
|
||||||
|
|
|
@ -17,6 +17,21 @@ ATTR_CHANGED_BY: Final = "changed_by"
|
||||||
ATTR_CODE_ARM_REQUIRED: Final = "code_arm_required"
|
ATTR_CODE_ARM_REQUIRED: Final = "code_arm_required"
|
||||||
|
|
||||||
|
|
||||||
|
class AlarmControlPanelState(StrEnum):
|
||||||
|
"""Alarm control panel entity states."""
|
||||||
|
|
||||||
|
DISARMED = "disarmed"
|
||||||
|
ARMED_HOME = "armed_home"
|
||||||
|
ARMED_AWAY = "armed_away"
|
||||||
|
ARMED_NIGHT = "armed_night"
|
||||||
|
ARMED_VACATION = "armed_vacation"
|
||||||
|
ARMED_CUSTOM_BYPASS = "armed_custom_bypass"
|
||||||
|
PENDING = "pending"
|
||||||
|
ARMING = "arming"
|
||||||
|
DISARMING = "disarming"
|
||||||
|
TRIGGERED = "triggered"
|
||||||
|
|
||||||
|
|
||||||
class CodeFormat(StrEnum):
|
class CodeFormat(StrEnum):
|
||||||
"""Code formats for the Alarm Control Panel."""
|
"""Code formats for the Alarm Control Panel."""
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,6 @@ from homeassistant.const import (
|
||||||
CONF_DOMAIN,
|
CONF_DOMAIN,
|
||||||
CONF_ENTITY_ID,
|
CONF_ENTITY_ID,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
|
||||||
STATE_ALARM_ARMED_VACATION,
|
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
STATE_ALARM_TRIGGERED,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
|
@ -31,7 +24,7 @@ from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
|
||||||
from homeassistant.helpers.entity import get_supported_features
|
from homeassistant.helpers.entity import get_supported_features
|
||||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||||
|
|
||||||
from . import DOMAIN
|
from . import DOMAIN, AlarmControlPanelState
|
||||||
from .const import (
|
from .const import (
|
||||||
CONDITION_ARMED_AWAY,
|
CONDITION_ARMED_AWAY,
|
||||||
CONDITION_ARMED_CUSTOM_BYPASS,
|
CONDITION_ARMED_CUSTOM_BYPASS,
|
||||||
|
@ -109,19 +102,19 @@ def async_condition_from_config(
|
||||||
) -> condition.ConditionCheckerType:
|
) -> condition.ConditionCheckerType:
|
||||||
"""Create a function to test a device condition."""
|
"""Create a function to test a device condition."""
|
||||||
if config[CONF_TYPE] == CONDITION_TRIGGERED:
|
if config[CONF_TYPE] == CONDITION_TRIGGERED:
|
||||||
state = STATE_ALARM_TRIGGERED
|
state = AlarmControlPanelState.TRIGGERED
|
||||||
elif config[CONF_TYPE] == CONDITION_DISARMED:
|
elif config[CONF_TYPE] == CONDITION_DISARMED:
|
||||||
state = STATE_ALARM_DISARMED
|
state = AlarmControlPanelState.DISARMED
|
||||||
elif config[CONF_TYPE] == CONDITION_ARMED_HOME:
|
elif config[CONF_TYPE] == CONDITION_ARMED_HOME:
|
||||||
state = STATE_ALARM_ARMED_HOME
|
state = AlarmControlPanelState.ARMED_HOME
|
||||||
elif config[CONF_TYPE] == CONDITION_ARMED_AWAY:
|
elif config[CONF_TYPE] == CONDITION_ARMED_AWAY:
|
||||||
state = STATE_ALARM_ARMED_AWAY
|
state = AlarmControlPanelState.ARMED_AWAY
|
||||||
elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT:
|
elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT:
|
||||||
state = STATE_ALARM_ARMED_NIGHT
|
state = AlarmControlPanelState.ARMED_NIGHT
|
||||||
elif config[CONF_TYPE] == CONDITION_ARMED_VACATION:
|
elif config[CONF_TYPE] == CONDITION_ARMED_VACATION:
|
||||||
state = STATE_ALARM_ARMED_VACATION
|
state = AlarmControlPanelState.ARMED_VACATION
|
||||||
elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS:
|
elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS:
|
||||||
state = STATE_ALARM_ARMED_CUSTOM_BYPASS
|
state = AlarmControlPanelState.ARMED_CUSTOM_BYPASS
|
||||||
|
|
||||||
registry = er.async_get(hass)
|
registry = er.async_get(hass)
|
||||||
entity_id = er.async_resolve_entity_id(registry, config[ATTR_ENTITY_ID])
|
entity_id = er.async_resolve_entity_id(registry, config[ATTR_ENTITY_ID])
|
||||||
|
|
|
@ -15,13 +15,6 @@ from homeassistant.const import (
|
||||||
CONF_FOR,
|
CONF_FOR,
|
||||||
CONF_PLATFORM,
|
CONF_PLATFORM,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
|
||||||
STATE_ALARM_ARMED_VACATION,
|
|
||||||
STATE_ALARM_ARMING,
|
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
STATE_ALARM_TRIGGERED,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||||
|
@ -29,7 +22,7 @@ from homeassistant.helpers.entity import get_supported_features
|
||||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from . import DOMAIN
|
from . import DOMAIN, AlarmControlPanelState
|
||||||
from .const import AlarmControlPanelEntityFeature
|
from .const import AlarmControlPanelEntityFeature
|
||||||
|
|
||||||
BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"}
|
BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"}
|
||||||
|
@ -129,19 +122,19 @@ async def async_attach_trigger(
|
||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Attach a trigger."""
|
"""Attach a trigger."""
|
||||||
if config[CONF_TYPE] == "triggered":
|
if config[CONF_TYPE] == "triggered":
|
||||||
to_state = STATE_ALARM_TRIGGERED
|
to_state = AlarmControlPanelState.TRIGGERED
|
||||||
elif config[CONF_TYPE] == "disarmed":
|
elif config[CONF_TYPE] == "disarmed":
|
||||||
to_state = STATE_ALARM_DISARMED
|
to_state = AlarmControlPanelState.DISARMED
|
||||||
elif config[CONF_TYPE] == "arming":
|
elif config[CONF_TYPE] == "arming":
|
||||||
to_state = STATE_ALARM_ARMING
|
to_state = AlarmControlPanelState.ARMING
|
||||||
elif config[CONF_TYPE] == "armed_home":
|
elif config[CONF_TYPE] == "armed_home":
|
||||||
to_state = STATE_ALARM_ARMED_HOME
|
to_state = AlarmControlPanelState.ARMED_HOME
|
||||||
elif config[CONF_TYPE] == "armed_away":
|
elif config[CONF_TYPE] == "armed_away":
|
||||||
to_state = STATE_ALARM_ARMED_AWAY
|
to_state = AlarmControlPanelState.ARMED_AWAY
|
||||||
elif config[CONF_TYPE] == "armed_night":
|
elif config[CONF_TYPE] == "armed_night":
|
||||||
to_state = STATE_ALARM_ARMED_NIGHT
|
to_state = AlarmControlPanelState.ARMED_NIGHT
|
||||||
elif config[CONF_TYPE] == "armed_vacation":
|
elif config[CONF_TYPE] == "armed_vacation":
|
||||||
to_state = STATE_ALARM_ARMED_VACATION
|
to_state = AlarmControlPanelState.ARMED_VACATION
|
||||||
|
|
||||||
state_config = {
|
state_config = {
|
||||||
state_trigger.CONF_PLATFORM: "state",
|
state_trigger.CONF_PLATFORM: "state",
|
||||||
|
|
|
@ -16,28 +16,21 @@ from homeassistant.const import (
|
||||||
SERVICE_ALARM_ARM_VACATION,
|
SERVICE_ALARM_ARM_VACATION,
|
||||||
SERVICE_ALARM_DISARM,
|
SERVICE_ALARM_DISARM,
|
||||||
SERVICE_ALARM_TRIGGER,
|
SERVICE_ALARM_TRIGGER,
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
|
||||||
STATE_ALARM_ARMED_VACATION,
|
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
STATE_ALARM_TRIGGERED,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import Context, HomeAssistant, State
|
from homeassistant.core import Context, HomeAssistant, State
|
||||||
|
|
||||||
from . import DOMAIN
|
from . import DOMAIN, AlarmControlPanelState
|
||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
|
||||||
VALID_STATES: Final[set[str]] = {
|
VALID_STATES: Final[set[str]] = {
|
||||||
STATE_ALARM_ARMED_AWAY,
|
AlarmControlPanelState.ARMED_AWAY,
|
||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
|
||||||
STATE_ALARM_ARMED_HOME,
|
AlarmControlPanelState.ARMED_HOME,
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
AlarmControlPanelState.ARMED_NIGHT,
|
||||||
STATE_ALARM_ARMED_VACATION,
|
AlarmControlPanelState.ARMED_VACATION,
|
||||||
STATE_ALARM_DISARMED,
|
AlarmControlPanelState.DISARMED,
|
||||||
STATE_ALARM_TRIGGERED,
|
AlarmControlPanelState.TRIGGERED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,19 +58,19 @@ async def _async_reproduce_state(
|
||||||
|
|
||||||
service_data = {ATTR_ENTITY_ID: state.entity_id}
|
service_data = {ATTR_ENTITY_ID: state.entity_id}
|
||||||
|
|
||||||
if state.state == STATE_ALARM_ARMED_AWAY:
|
if state.state == AlarmControlPanelState.ARMED_AWAY:
|
||||||
service = SERVICE_ALARM_ARM_AWAY
|
service = SERVICE_ALARM_ARM_AWAY
|
||||||
elif state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
|
elif state.state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS:
|
||||||
service = SERVICE_ALARM_ARM_CUSTOM_BYPASS
|
service = SERVICE_ALARM_ARM_CUSTOM_BYPASS
|
||||||
elif state.state == STATE_ALARM_ARMED_HOME:
|
elif state.state == AlarmControlPanelState.ARMED_HOME:
|
||||||
service = SERVICE_ALARM_ARM_HOME
|
service = SERVICE_ALARM_ARM_HOME
|
||||||
elif state.state == STATE_ALARM_ARMED_NIGHT:
|
elif state.state == AlarmControlPanelState.ARMED_NIGHT:
|
||||||
service = SERVICE_ALARM_ARM_NIGHT
|
service = SERVICE_ALARM_ARM_NIGHT
|
||||||
elif state.state == STATE_ALARM_ARMED_VACATION:
|
elif state.state == AlarmControlPanelState.ARMED_VACATION:
|
||||||
service = SERVICE_ALARM_ARM_VACATION
|
service = SERVICE_ALARM_ARM_VACATION
|
||||||
elif state.state == STATE_ALARM_DISARMED:
|
elif state.state == AlarmControlPanelState.DISARMED:
|
||||||
service = SERVICE_ALARM_DISARM
|
service = SERVICE_ALARM_DISARM
|
||||||
elif state.state == STATE_ALARM_TRIGGERED:
|
elif state.state == AlarmControlPanelState.TRIGGERED:
|
||||||
service = SERVICE_ALARM_TRIGGER
|
service = SERVICE_ALARM_TRIGGER
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
|
|
|
@ -7,16 +7,10 @@ import voluptuous as vol
|
||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
AlarmControlPanelEntity,
|
AlarmControlPanelEntity,
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
|
AlarmControlPanelState,
|
||||||
CodeFormat,
|
CodeFormat,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import ATTR_CODE
|
||||||
ATTR_CODE,
|
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
STATE_ALARM_TRIGGERED,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_platform
|
from homeassistant.helpers import entity_platform
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
@ -106,15 +100,15 @@ class AlarmDecoderAlarmPanel(AlarmDecoderEntity, AlarmControlPanelEntity):
|
||||||
def _message_callback(self, message):
|
def _message_callback(self, message):
|
||||||
"""Handle received messages."""
|
"""Handle received messages."""
|
||||||
if message.alarm_sounding or message.fire_alarm:
|
if message.alarm_sounding or message.fire_alarm:
|
||||||
self._attr_state = STATE_ALARM_TRIGGERED
|
self._attr_alarm_state = AlarmControlPanelState.TRIGGERED
|
||||||
elif message.armed_away:
|
elif message.armed_away:
|
||||||
self._attr_state = STATE_ALARM_ARMED_AWAY
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
|
||||||
elif message.armed_home and (message.entry_delay_off or message.perimeter_only):
|
elif message.armed_home and (message.entry_delay_off or message.perimeter_only):
|
||||||
self._attr_state = STATE_ALARM_ARMED_NIGHT
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
|
||||||
elif message.armed_home:
|
elif message.armed_home:
|
||||||
self._attr_state = STATE_ALARM_ARMED_HOME
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
|
||||||
else:
|
else:
|
||||||
self._attr_state = STATE_ALARM_DISARMED
|
self._attr_alarm_state = AlarmControlPanelState.DISARMED
|
||||||
|
|
||||||
self._attr_extra_state_attributes = {
|
self._attr_extra_state_attributes = {
|
||||||
"ac_power": message.ac_power,
|
"ac_power": message.ac_power,
|
||||||
|
|
|
@ -26,6 +26,7 @@ from homeassistant.components import (
|
||||||
)
|
)
|
||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
|
AlarmControlPanelState,
|
||||||
CodeFormat,
|
CodeFormat,
|
||||||
)
|
)
|
||||||
from homeassistant.components.climate import HVACMode
|
from homeassistant.components.climate import HVACMode
|
||||||
|
@ -36,10 +37,6 @@ from homeassistant.const import (
|
||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
|
||||||
STATE_IDLE,
|
STATE_IDLE,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
|
@ -1317,13 +1314,13 @@ class AlexaSecurityPanelController(AlexaCapability):
|
||||||
raise UnsupportedProperty(name)
|
raise UnsupportedProperty(name)
|
||||||
|
|
||||||
arm_state = self.entity.state
|
arm_state = self.entity.state
|
||||||
if arm_state == STATE_ALARM_ARMED_HOME:
|
if arm_state == AlarmControlPanelState.ARMED_HOME:
|
||||||
return "ARMED_STAY"
|
return "ARMED_STAY"
|
||||||
if arm_state == STATE_ALARM_ARMED_AWAY:
|
if arm_state == AlarmControlPanelState.ARMED_AWAY:
|
||||||
return "ARMED_AWAY"
|
return "ARMED_AWAY"
|
||||||
if arm_state == STATE_ALARM_ARMED_NIGHT:
|
if arm_state == AlarmControlPanelState.ARMED_NIGHT:
|
||||||
return "ARMED_NIGHT"
|
return "ARMED_NIGHT"
|
||||||
if arm_state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
|
if arm_state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS:
|
||||||
return "ARMED_STAY"
|
return "ARMED_STAY"
|
||||||
return "DISARMED"
|
return "DISARMED"
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ from typing import Any
|
||||||
|
|
||||||
from homeassistant import core as ha
|
from homeassistant import core as ha
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
|
alarm_control_panel,
|
||||||
button,
|
button,
|
||||||
camera,
|
camera,
|
||||||
climate,
|
climate,
|
||||||
|
@ -51,7 +52,6 @@ from homeassistant.const import (
|
||||||
SERVICE_VOLUME_MUTE,
|
SERVICE_VOLUME_MUTE,
|
||||||
SERVICE_VOLUME_SET,
|
SERVICE_VOLUME_SET,
|
||||||
SERVICE_VOLUME_UP,
|
SERVICE_VOLUME_UP,
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import network
|
from homeassistant.helpers import network
|
||||||
|
@ -1083,7 +1083,13 @@ async def async_api_arm(
|
||||||
arm_state = directive.payload["armState"]
|
arm_state = directive.payload["armState"]
|
||||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||||
|
|
||||||
if entity.state != STATE_ALARM_DISARMED:
|
# Per Alexa Documentation: users are not allowed to switch from armed_away
|
||||||
|
# directly to another armed state without first disarming the system.
|
||||||
|
# https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-securitypanelcontroller.html#arming
|
||||||
|
if (
|
||||||
|
entity.state == alarm_control_panel.AlarmControlPanelState.ARMED_AWAY
|
||||||
|
and arm_state != "ARMED_AWAY"
|
||||||
|
):
|
||||||
msg = "You must disarm the system before you can set the requested arm state."
|
msg = "You must disarm the system before you can set the requested arm state."
|
||||||
raise AlexaSecurityPanelAuthorizationRequired(msg)
|
raise AlexaSecurityPanelAuthorizationRequired(msg)
|
||||||
|
|
||||||
|
@ -1133,7 +1139,7 @@ async def async_api_disarm(
|
||||||
# Per Alexa Documentation: If you receive a Disarm directive, and the
|
# Per Alexa Documentation: If you receive a Disarm directive, and the
|
||||||
# system is already disarmed, respond with a success response,
|
# system is already disarmed, respond with a success response,
|
||||||
# not an error response.
|
# not an error response.
|
||||||
if entity.state == STATE_ALARM_DISARMED:
|
if entity.state == alarm_control_panel.AlarmControlPanelState.DISARMED:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
payload = directive.payload
|
payload = directive.payload
|
||||||
|
|
|
@ -29,6 +29,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.entity_registry as er
|
import homeassistant.helpers.entity_registry as er
|
||||||
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.helpers.system_info import async_get_system_info
|
from homeassistant.helpers.system_info import async_get_system_info
|
||||||
from homeassistant.loader import (
|
from homeassistant.loader import (
|
||||||
|
@ -136,7 +137,7 @@ class Analytics:
|
||||||
@property
|
@property
|
||||||
def supervisor(self) -> bool:
|
def supervisor(self) -> bool:
|
||||||
"""Return bool if a supervisor is present."""
|
"""Return bool if a supervisor is present."""
|
||||||
return hassio.is_hassio(self.hass)
|
return is_hassio(self.hass)
|
||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Load preferences."""
|
"""Load preferences."""
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"domain": "analytics",
|
"domain": "analytics",
|
||||||
"name": "Analytics",
|
"name": "Analytics",
|
||||||
"after_dependencies": ["energy", "recorder"],
|
"after_dependencies": ["energy", "hassio", "recorder"],
|
||||||
"codeowners": ["@home-assistant/core", "@ludeeus"],
|
"codeowners": ["@home-assistant/core", "@ludeeus"],
|
||||||
"dependencies": ["api", "websocket_api"],
|
"dependencies": ["api", "websocket_api"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
||||||
|
|
|
@ -16,7 +16,6 @@ from homeassistant.config_entries import (
|
||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
OptionsFlow,
|
OptionsFlow,
|
||||||
OptionsFlowWithConfigEntry,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
@ -27,6 +26,7 @@ from homeassistant.helpers.selector import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_TRACKED_ADDONS,
|
||||||
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
||||||
CONF_TRACKED_INTEGRATIONS,
|
CONF_TRACKED_INTEGRATIONS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -45,9 +45,11 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
def async_get_options_flow(
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
) -> HomeassistantAnalyticsOptionsFlowHandler:
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return HomeassistantAnalyticsOptionsFlowHandler(config_entry)
|
return HomeassistantAnalyticsOptionsFlowHandler()
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
@ -55,8 +57,12 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
|
if all(
|
||||||
CONF_TRACKED_CUSTOM_INTEGRATIONS
|
[
|
||||||
|
not user_input.get(CONF_TRACKED_ADDONS),
|
||||||
|
not user_input.get(CONF_TRACKED_INTEGRATIONS),
|
||||||
|
not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
|
||||||
|
]
|
||||||
):
|
):
|
||||||
errors["base"] = "no_integrations_selected"
|
errors["base"] = "no_integrations_selected"
|
||||||
else:
|
else:
|
||||||
|
@ -64,6 +70,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
title="Home Assistant Analytics Insights",
|
title="Home Assistant Analytics Insights",
|
||||||
data={},
|
data={},
|
||||||
options={
|
options={
|
||||||
|
CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
|
||||||
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
||||||
CONF_TRACKED_INTEGRATIONS, []
|
CONF_TRACKED_INTEGRATIONS, []
|
||||||
),
|
),
|
||||||
|
@ -77,6 +84,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
session=async_get_clientsession(self.hass)
|
session=async_get_clientsession(self.hass)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
addons = await client.get_addons()
|
||||||
integrations = await client.get_integrations()
|
integrations = await client.get_integrations()
|
||||||
custom_integrations = await client.get_custom_integrations()
|
custom_integrations = await client.get_custom_integrations()
|
||||||
except HomeassistantAnalyticsConnectionError:
|
except HomeassistantAnalyticsConnectionError:
|
||||||
|
@ -99,6 +107,13 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
errors=errors,
|
errors=errors,
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
|
vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=list(addons),
|
||||||
|
multiple=True,
|
||||||
|
sort=True,
|
||||||
|
)
|
||||||
|
),
|
||||||
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||||
SelectSelectorConfig(
|
SelectSelectorConfig(
|
||||||
options=options,
|
options=options,
|
||||||
|
@ -118,7 +133,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
|
||||||
"""Handle Homeassistant Analytics options."""
|
"""Handle Homeassistant Analytics options."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
|
@ -127,14 +142,19 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||||
"""Manage the options."""
|
"""Manage the options."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
|
if all(
|
||||||
CONF_TRACKED_CUSTOM_INTEGRATIONS
|
[
|
||||||
|
not user_input.get(CONF_TRACKED_ADDONS),
|
||||||
|
not user_input.get(CONF_TRACKED_INTEGRATIONS),
|
||||||
|
not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
|
||||||
|
]
|
||||||
):
|
):
|
||||||
errors["base"] = "no_integrations_selected"
|
errors["base"] = "no_integrations_selected"
|
||||||
else:
|
else:
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title="",
|
title="",
|
||||||
data={
|
data={
|
||||||
|
CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
|
||||||
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
||||||
CONF_TRACKED_INTEGRATIONS, []
|
CONF_TRACKED_INTEGRATIONS, []
|
||||||
),
|
),
|
||||||
|
@ -148,6 +168,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||||
session=async_get_clientsession(self.hass)
|
session=async_get_clientsession(self.hass)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
addons = await client.get_addons()
|
||||||
integrations = await client.get_integrations()
|
integrations = await client.get_integrations()
|
||||||
custom_integrations = await client.get_custom_integrations()
|
custom_integrations = await client.get_custom_integrations()
|
||||||
except HomeassistantAnalyticsConnectionError:
|
except HomeassistantAnalyticsConnectionError:
|
||||||
|
@ -168,6 +189,13 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||||
data_schema=self.add_suggested_values_to_schema(
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
vol.Schema(
|
vol.Schema(
|
||||||
{
|
{
|
||||||
|
vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=list(addons),
|
||||||
|
multiple=True,
|
||||||
|
sort=True,
|
||||||
|
)
|
||||||
|
),
|
||||||
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||||
SelectSelectorConfig(
|
SelectSelectorConfig(
|
||||||
options=options,
|
options=options,
|
||||||
|
@ -184,6 +212,6 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
self.options,
|
self.config_entry.options,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import logging
|
||||||
|
|
||||||
DOMAIN = "analytics_insights"
|
DOMAIN = "analytics_insights"
|
||||||
|
|
||||||
|
CONF_TRACKED_ADDONS = "tracked_addons"
|
||||||
CONF_TRACKED_INTEGRATIONS = "tracked_integrations"
|
CONF_TRACKED_INTEGRATIONS = "tracked_integrations"
|
||||||
CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations"
|
CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations"
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,13 @@ from python_homeassistant_analytics import (
|
||||||
HomeassistantAnalyticsConnectionError,
|
HomeassistantAnalyticsConnectionError,
|
||||||
HomeassistantAnalyticsNotModifiedError,
|
HomeassistantAnalyticsNotModifiedError,
|
||||||
)
|
)
|
||||||
|
from python_homeassistant_analytics.models import Addon
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_TRACKED_ADDONS,
|
||||||
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
||||||
CONF_TRACKED_INTEGRATIONS,
|
CONF_TRACKED_INTEGRATIONS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -33,6 +35,7 @@ class AnalyticsData:
|
||||||
|
|
||||||
active_installations: int
|
active_installations: int
|
||||||
reports_integrations: int
|
reports_integrations: int
|
||||||
|
addons: dict[str, int]
|
||||||
core_integrations: dict[str, int]
|
core_integrations: dict[str, int]
|
||||||
custom_integrations: dict[str, int]
|
custom_integrations: dict[str, int]
|
||||||
|
|
||||||
|
@ -53,6 +56,7 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
||||||
update_interval=timedelta(hours=12),
|
update_interval=timedelta(hours=12),
|
||||||
)
|
)
|
||||||
self._client = client
|
self._client = client
|
||||||
|
self._tracked_addons = self.config_entry.options.get(CONF_TRACKED_ADDONS, [])
|
||||||
self._tracked_integrations = self.config_entry.options[
|
self._tracked_integrations = self.config_entry.options[
|
||||||
CONF_TRACKED_INTEGRATIONS
|
CONF_TRACKED_INTEGRATIONS
|
||||||
]
|
]
|
||||||
|
@ -62,6 +66,7 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
||||||
|
|
||||||
async def _async_update_data(self) -> AnalyticsData:
|
async def _async_update_data(self) -> AnalyticsData:
|
||||||
try:
|
try:
|
||||||
|
addons_data = await self._client.get_addons()
|
||||||
data = await self._client.get_current_analytics()
|
data = await self._client.get_current_analytics()
|
||||||
custom_data = await self._client.get_custom_integrations()
|
custom_data = await self._client.get_custom_integrations()
|
||||||
except HomeassistantAnalyticsConnectionError as err:
|
except HomeassistantAnalyticsConnectionError as err:
|
||||||
|
@ -70,6 +75,9 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
||||||
) from err
|
) from err
|
||||||
except HomeassistantAnalyticsNotModifiedError:
|
except HomeassistantAnalyticsNotModifiedError:
|
||||||
return self.data
|
return self.data
|
||||||
|
addons = {
|
||||||
|
addon: get_addon_value(addons_data, addon) for addon in self._tracked_addons
|
||||||
|
}
|
||||||
core_integrations = {
|
core_integrations = {
|
||||||
integration: data.integrations.get(integration, 0)
|
integration: data.integrations.get(integration, 0)
|
||||||
for integration in self._tracked_integrations
|
for integration in self._tracked_integrations
|
||||||
|
@ -81,11 +89,19 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
||||||
return AnalyticsData(
|
return AnalyticsData(
|
||||||
data.active_installations,
|
data.active_installations,
|
||||||
data.reports_integrations,
|
data.reports_integrations,
|
||||||
|
addons,
|
||||||
core_integrations,
|
core_integrations,
|
||||||
custom_integrations,
|
custom_integrations,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_addon_value(data: dict[str, Addon], name_slug: str) -> int:
|
||||||
|
"""Get addon value."""
|
||||||
|
if name_slug in data:
|
||||||
|
return data[name_slug].total
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def get_custom_integration_value(
|
def get_custom_integration_value(
|
||||||
data: dict[str, CustomIntegration], domain: str
|
data: dict[str, CustomIntegration], domain: str
|
||||||
) -> int:
|
) -> int:
|
||||||
|
|
|
@ -29,6 +29,20 @@ class AnalyticsSensorEntityDescription(SensorEntityDescription):
|
||||||
value_fn: Callable[[AnalyticsData], StateType]
|
value_fn: Callable[[AnalyticsData], StateType]
|
||||||
|
|
||||||
|
|
||||||
|
def get_addon_entity_description(
|
||||||
|
name_slug: str,
|
||||||
|
) -> AnalyticsSensorEntityDescription:
|
||||||
|
"""Get addon entity description."""
|
||||||
|
return AnalyticsSensorEntityDescription(
|
||||||
|
key=f"addon_{name_slug}_active_installations",
|
||||||
|
translation_key="addons",
|
||||||
|
name=name_slug,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
native_unit_of_measurement="active installations",
|
||||||
|
value_fn=lambda data: data.addons.get(name_slug),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_core_integration_entity_description(
|
def get_core_integration_entity_description(
|
||||||
domain: str, name: str
|
domain: str, name: str
|
||||||
) -> AnalyticsSensorEntityDescription:
|
) -> AnalyticsSensorEntityDescription:
|
||||||
|
@ -89,6 +103,13 @@ async def async_setup_entry(
|
||||||
analytics_data.coordinator
|
analytics_data.coordinator
|
||||||
)
|
)
|
||||||
entities: list[HomeassistantAnalyticsSensor] = []
|
entities: list[HomeassistantAnalyticsSensor] = []
|
||||||
|
entities.extend(
|
||||||
|
HomeassistantAnalyticsSensor(
|
||||||
|
coordinator,
|
||||||
|
get_addon_entity_description(addon_name_slug),
|
||||||
|
)
|
||||||
|
for addon_name_slug in coordinator.data.addons
|
||||||
|
)
|
||||||
entities.extend(
|
entities.extend(
|
||||||
HomeassistantAnalyticsSensor(
|
HomeassistantAnalyticsSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
|
|
|
@ -3,10 +3,12 @@
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"tracked_addons": "Addons",
|
||||||
"tracked_integrations": "Integrations",
|
"tracked_integrations": "Integrations",
|
||||||
"tracked_custom_integrations": "Custom integrations"
|
"tracked_custom_integrations": "Custom integrations"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
|
"tracked_addons": "Select the addons you want to track",
|
||||||
"tracked_integrations": "Select the integrations you want to track",
|
"tracked_integrations": "Select the integrations you want to track",
|
||||||
"tracked_custom_integrations": "Select the custom integrations you want to track"
|
"tracked_custom_integrations": "Select the custom integrations you want to track"
|
||||||
}
|
}
|
||||||
|
@ -24,10 +26,12 @@
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"tracked_addons": "[%key:component::analytics_insights::config::step::user::data::tracked_addons%]",
|
||||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]",
|
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]",
|
||||||
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]"
|
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
|
"tracked_addons": "[%key:component::analytics_insights::config::step::user::data_description::tracked_addons%]",
|
||||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]",
|
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]",
|
||||||
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]"
|
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@ -40,6 +41,7 @@ from .const import (
|
||||||
CONF_ADB_SERVER_IP,
|
CONF_ADB_SERVER_IP,
|
||||||
CONF_ADB_SERVER_PORT,
|
CONF_ADB_SERVER_PORT,
|
||||||
CONF_ADBKEY,
|
CONF_ADBKEY,
|
||||||
|
CONF_SCREENCAP_INTERVAL,
|
||||||
CONF_STATE_DETECTION_RULES,
|
CONF_STATE_DETECTION_RULES,
|
||||||
DEFAULT_ADB_SERVER_PORT,
|
DEFAULT_ADB_SERVER_PORT,
|
||||||
DEVICE_ANDROIDTV,
|
DEVICE_ANDROIDTV,
|
||||||
|
@ -66,6 +68,8 @@ RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
|
||||||
|
|
||||||
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
|
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AndroidTVRuntimeData:
|
class AndroidTVRuntimeData:
|
||||||
|
@ -157,6 +161,32 @@ async def async_connect_androidtv(
|
||||||
return aftv, None
|
return aftv, None
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Migrate old entry."""
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Migrating configuration from version %s.%s", entry.version, entry.minor_version
|
||||||
|
)
|
||||||
|
|
||||||
|
if entry.version == 1:
|
||||||
|
new_options = {**entry.options}
|
||||||
|
|
||||||
|
# Migrate MinorVersion 1 -> MinorVersion 2: New option
|
||||||
|
if entry.minor_version < 2:
|
||||||
|
new_options = {**new_options, CONF_SCREENCAP_INTERVAL: 0}
|
||||||
|
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry, options=new_options, minor_version=2, version=1
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Migration to configuration version %s.%s successful",
|
||||||
|
entry.version,
|
||||||
|
entry.minor_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool:
|
||||||
"""Set up Android Debug Bridge platform."""
|
"""Set up Android Debug Bridge platform."""
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.config_entries import (
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
OptionsFlowWithConfigEntry,
|
OptionsFlow,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT
|
from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
@ -34,7 +34,7 @@ from .const import (
|
||||||
CONF_APPS,
|
CONF_APPS,
|
||||||
CONF_EXCLUDE_UNNAMED_APPS,
|
CONF_EXCLUDE_UNNAMED_APPS,
|
||||||
CONF_GET_SOURCES,
|
CONF_GET_SOURCES,
|
||||||
CONF_SCREENCAP,
|
CONF_SCREENCAP_INTERVAL,
|
||||||
CONF_STATE_DETECTION_RULES,
|
CONF_STATE_DETECTION_RULES,
|
||||||
CONF_TURN_OFF_COMMAND,
|
CONF_TURN_OFF_COMMAND,
|
||||||
CONF_TURN_ON_COMMAND,
|
CONF_TURN_ON_COMMAND,
|
||||||
|
@ -43,7 +43,7 @@ from .const import (
|
||||||
DEFAULT_EXCLUDE_UNNAMED_APPS,
|
DEFAULT_EXCLUDE_UNNAMED_APPS,
|
||||||
DEFAULT_GET_SOURCES,
|
DEFAULT_GET_SOURCES,
|
||||||
DEFAULT_PORT,
|
DEFAULT_PORT,
|
||||||
DEFAULT_SCREENCAP,
|
DEFAULT_SCREENCAP_INTERVAL,
|
||||||
DEVICE_CLASSES,
|
DEVICE_CLASSES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PROP_ETHMAC,
|
PROP_ETHMAC,
|
||||||
|
@ -76,6 +76,7 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow."""
|
"""Handle a config flow."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
MINOR_VERSION = 2
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _show_setup_form(
|
def _show_setup_form(
|
||||||
|
@ -185,16 +186,14 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
return OptionsFlowHandler(config_entry)
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
class OptionsFlowHandler(OptionsFlowWithConfigEntry):
|
class OptionsFlowHandler(OptionsFlow):
|
||||||
"""Handle an option flow for Android Debug Bridge."""
|
"""Handle an option flow for Android Debug Bridge."""
|
||||||
|
|
||||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||||
"""Initialize options flow."""
|
"""Initialize options flow."""
|
||||||
super().__init__(config_entry)
|
self._apps: dict[str, Any] = dict(config_entry.options.get(CONF_APPS, {}))
|
||||||
|
self._state_det_rules: dict[str, Any] = dict(
|
||||||
self._apps: dict[str, Any] = self.options.setdefault(CONF_APPS, {})
|
config_entry.options.get(CONF_STATE_DETECTION_RULES, {})
|
||||||
self._state_det_rules: dict[str, Any] = self.options.setdefault(
|
|
||||||
CONF_STATE_DETECTION_RULES, {}
|
|
||||||
)
|
)
|
||||||
self._conf_app_id: str | None = None
|
self._conf_app_id: str | None = None
|
||||||
self._conf_rule_id: str | None = None
|
self._conf_rule_id: str | None = None
|
||||||
|
@ -236,7 +235,7 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||||
SelectOptionDict(value=k, label=v) for k, v in apps_list.items()
|
SelectOptionDict(value=k, label=v) for k, v in apps_list.items()
|
||||||
]
|
]
|
||||||
rules = [RULES_NEW_ID, *self._state_det_rules]
|
rules = [RULES_NEW_ID, *self._state_det_rules]
|
||||||
options = self.options
|
options = self.config_entry.options
|
||||||
|
|
||||||
data_schema = vol.Schema(
|
data_schema = vol.Schema(
|
||||||
{
|
{
|
||||||
|
@ -253,10 +252,12 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||||
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
|
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
|
||||||
),
|
),
|
||||||
): bool,
|
): bool,
|
||||||
vol.Optional(
|
vol.Required(
|
||||||
CONF_SCREENCAP,
|
CONF_SCREENCAP_INTERVAL,
|
||||||
default=options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP),
|
default=options.get(
|
||||||
): bool,
|
CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
|
||||||
|
),
|
||||||
|
): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=15)),
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_TURN_OFF_COMMAND,
|
CONF_TURN_OFF_COMMAND,
|
||||||
description={
|
description={
|
||||||
|
|
|
@ -9,6 +9,7 @@ CONF_APPS = "apps"
|
||||||
CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps"
|
CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps"
|
||||||
CONF_GET_SOURCES = "get_sources"
|
CONF_GET_SOURCES = "get_sources"
|
||||||
CONF_SCREENCAP = "screencap"
|
CONF_SCREENCAP = "screencap"
|
||||||
|
CONF_SCREENCAP_INTERVAL = "screencap_interval"
|
||||||
CONF_STATE_DETECTION_RULES = "state_detection_rules"
|
CONF_STATE_DETECTION_RULES = "state_detection_rules"
|
||||||
CONF_TURN_OFF_COMMAND = "turn_off_command"
|
CONF_TURN_OFF_COMMAND = "turn_off_command"
|
||||||
CONF_TURN_ON_COMMAND = "turn_on_command"
|
CONF_TURN_ON_COMMAND = "turn_on_command"
|
||||||
|
@ -18,7 +19,7 @@ DEFAULT_DEVICE_CLASS = "auto"
|
||||||
DEFAULT_EXCLUDE_UNNAMED_APPS = False
|
DEFAULT_EXCLUDE_UNNAMED_APPS = False
|
||||||
DEFAULT_GET_SOURCES = True
|
DEFAULT_GET_SOURCES = True
|
||||||
DEFAULT_PORT = 5555
|
DEFAULT_PORT = 5555
|
||||||
DEFAULT_SCREENCAP = True
|
DEFAULT_SCREENCAP_INTERVAL = 5
|
||||||
|
|
||||||
DEVICE_ANDROIDTV = "androidtv"
|
DEVICE_ANDROIDTV = "androidtv"
|
||||||
DEVICE_FIRETV = "firetv"
|
DEVICE_FIRETV = "firetv"
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from androidtv.constants import APPS, KEYS
|
from androidtv.constants import APPS, KEYS
|
||||||
from androidtv.setup_async import AndroidTVAsync, FireTVAsync
|
from androidtv.setup_async import AndroidTVAsync, FireTVAsync
|
||||||
|
@ -23,19 +22,19 @@ from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from . import AndroidTVConfigEntry
|
from . import AndroidTVConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_APPS,
|
CONF_APPS,
|
||||||
CONF_EXCLUDE_UNNAMED_APPS,
|
CONF_EXCLUDE_UNNAMED_APPS,
|
||||||
CONF_GET_SOURCES,
|
CONF_GET_SOURCES,
|
||||||
CONF_SCREENCAP,
|
CONF_SCREENCAP_INTERVAL,
|
||||||
CONF_TURN_OFF_COMMAND,
|
CONF_TURN_OFF_COMMAND,
|
||||||
CONF_TURN_ON_COMMAND,
|
CONF_TURN_ON_COMMAND,
|
||||||
DEFAULT_EXCLUDE_UNNAMED_APPS,
|
DEFAULT_EXCLUDE_UNNAMED_APPS,
|
||||||
DEFAULT_GET_SOURCES,
|
DEFAULT_GET_SOURCES,
|
||||||
DEFAULT_SCREENCAP,
|
DEFAULT_SCREENCAP_INTERVAL,
|
||||||
DEVICE_ANDROIDTV,
|
DEVICE_ANDROIDTV,
|
||||||
SIGNAL_CONFIG_ENTITY,
|
SIGNAL_CONFIG_ENTITY,
|
||||||
)
|
)
|
||||||
|
@ -48,8 +47,6 @@ ATTR_DEVICE_PATH = "device_path"
|
||||||
ATTR_HDMI_INPUT = "hdmi_input"
|
ATTR_HDMI_INPUT = "hdmi_input"
|
||||||
ATTR_LOCAL_PATH = "local_path"
|
ATTR_LOCAL_PATH = "local_path"
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCREENCAPS = timedelta(seconds=60)
|
|
||||||
|
|
||||||
SERVICE_ADB_COMMAND = "adb_command"
|
SERVICE_ADB_COMMAND = "adb_command"
|
||||||
SERVICE_DOWNLOAD = "download"
|
SERVICE_DOWNLOAD = "download"
|
||||||
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
|
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
|
||||||
|
@ -125,7 +122,8 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
||||||
self._app_name_to_id: dict[str, str] = {}
|
self._app_name_to_id: dict[str, str] = {}
|
||||||
self._get_sources = DEFAULT_GET_SOURCES
|
self._get_sources = DEFAULT_GET_SOURCES
|
||||||
self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS
|
self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS
|
||||||
self._screencap = DEFAULT_SCREENCAP
|
self._screencap_delta: timedelta | None = None
|
||||||
|
self._last_screencap: datetime | None = None
|
||||||
self.turn_on_command: str | None = None
|
self.turn_on_command: str | None = None
|
||||||
self.turn_off_command: str | None = None
|
self.turn_off_command: str | None = None
|
||||||
|
|
||||||
|
@ -159,7 +157,13 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
||||||
self._exclude_unnamed_apps = options.get(
|
self._exclude_unnamed_apps = options.get(
|
||||||
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
|
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
|
||||||
)
|
)
|
||||||
self._screencap = options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP)
|
screencap_interval: int = options.get(
|
||||||
|
CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
|
||||||
|
)
|
||||||
|
if screencap_interval > 0:
|
||||||
|
self._screencap_delta = timedelta(minutes=screencap_interval)
|
||||||
|
else:
|
||||||
|
self._screencap_delta = None
|
||||||
self.turn_off_command = options.get(CONF_TURN_OFF_COMMAND)
|
self.turn_off_command = options.get(CONF_TURN_OFF_COMMAND)
|
||||||
self.turn_on_command = options.get(CONF_TURN_ON_COMMAND)
|
self.turn_on_command = options.get(CONF_TURN_ON_COMMAND)
|
||||||
|
|
||||||
|
@ -183,7 +187,7 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
||||||
async def _async_get_screencap(self, prev_app_id: str | None = None) -> None:
|
async def _async_get_screencap(self, prev_app_id: str | None = None) -> None:
|
||||||
"""Take a screen capture from the device when enabled."""
|
"""Take a screen capture from the device when enabled."""
|
||||||
if (
|
if (
|
||||||
not self._screencap
|
not self._screencap_delta
|
||||||
or self.state in {MediaPlayerState.OFF, None}
|
or self.state in {MediaPlayerState.OFF, None}
|
||||||
or not self.available
|
or not self.available
|
||||||
):
|
):
|
||||||
|
@ -193,11 +197,18 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
||||||
force: bool = prev_app_id is not None
|
force: bool = prev_app_id is not None
|
||||||
if force:
|
if force:
|
||||||
force = prev_app_id != self._attr_app_id
|
force = prev_app_id != self._attr_app_id
|
||||||
await self._adb_get_screencap(no_throttle=force)
|
await self._adb_get_screencap(force)
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCREENCAPS)
|
async def _adb_get_screencap(self, force: bool = False) -> None:
|
||||||
async def _adb_get_screencap(self, **kwargs: Any) -> None:
|
"""Take a screen capture from the device every configured minutes."""
|
||||||
"""Take a screen capture from the device every 60 seconds."""
|
time_elapsed = self._screencap_delta is not None and (
|
||||||
|
self._last_screencap is None
|
||||||
|
or (utcnow() - self._last_screencap) >= self._screencap_delta
|
||||||
|
)
|
||||||
|
if not (force or time_elapsed):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._last_screencap = utcnow()
|
||||||
if media_data := await self._adb_screencap():
|
if media_data := await self._adb_screencap():
|
||||||
self._media_image = media_data, "image/png"
|
self._media_image = media_data, "image/png"
|
||||||
self._attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16]
|
self._attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16]
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
"apps": "Configure applications list",
|
"apps": "Configure applications list",
|
||||||
"get_sources": "Retrieve the running apps as the list of sources",
|
"get_sources": "Retrieve the running apps as the list of sources",
|
||||||
"exclude_unnamed_apps": "Exclude apps with unknown name from the sources list",
|
"exclude_unnamed_apps": "Exclude apps with unknown name from the sources list",
|
||||||
"screencap": "Use screen capture for album art",
|
"screencap_interval": "Interval in minutes between screen capture for album art (set 0 to disable)",
|
||||||
"state_detection_rules": "Configure state detection rules",
|
"state_detection_rules": "Configure state detection rules",
|
||||||
"turn_off_command": "ADB shell turn off command (leave empty for default)",
|
"turn_off_command": "ADB shell turn off command (leave empty for default)",
|
||||||
"turn_on_command": "ADB shell turn on command (leave empty for default)"
|
"turn_on_command": "ADB shell turn on command (leave empty for default)"
|
||||||
|
|
|
@ -20,7 +20,7 @@ from homeassistant.config_entries import (
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
OptionsFlowWithConfigEntry,
|
OptionsFlow,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
@ -221,13 +221,12 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
return AndroidTVRemoteOptionsFlowHandler(config_entry)
|
return AndroidTVRemoteOptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
class AndroidTVRemoteOptionsFlowHandler(OptionsFlow):
|
||||||
"""Android TV Remote options flow."""
|
"""Android TV Remote options flow."""
|
||||||
|
|
||||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||||
"""Initialize options flow."""
|
"""Initialize options flow."""
|
||||||
super().__init__(config_entry)
|
self._apps: dict[str, Any] = dict(config_entry.options.get(CONF_APPS, {}))
|
||||||
self._apps: dict[str, Any] = self.options.setdefault(CONF_APPS, {})
|
|
||||||
self._conf_app_id: str | None = None
|
self._conf_app_id: str | None = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
@ -13,7 +13,7 @@ from anova_wifi import (
|
||||||
WebsocketFailure,
|
WebsocketFailure,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
@ -71,3 +71,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: AnovaConfigEntry) -> bo
|
||||||
# Disconnect from WS
|
# Disconnect from WS
|
||||||
await entry.runtime_data.api.disconnect_websocket()
|
await entry.runtime_data.api.disconnect_websocket()
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass: HomeAssistant, entry: AnovaConfigEntry) -> bool:
|
||||||
|
"""Migrate entry."""
|
||||||
|
_LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
|
||||||
|
|
||||||
|
if entry.version > 1:
|
||||||
|
# This means the user has downgraded from a future version
|
||||||
|
return False
|
||||||
|
|
||||||
|
if entry.version == 1 and entry.minor_version == 1:
|
||||||
|
new_data = {**entry.data}
|
||||||
|
if CONF_DEVICES in new_data:
|
||||||
|
new_data.pop(CONF_DEVICES)
|
||||||
|
|
||||||
|
hass.config_entries.async_update_entry(entry, data=new_data, minor_version=2)
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
|
@ -6,7 +6,7 @@ from anova_wifi import AnovaApi, InvalidLogin
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
@ -16,6 +16,7 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Sets up a config flow for Anova."""
|
"""Sets up a config flow for Anova."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
MINOR_VERSION = 2
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, str] | None = None
|
self, user_input: dict[str, str] | None = None
|
||||||
|
@ -42,8 +43,6 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
|
||||||
data={
|
data={
|
||||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||||
# this can be removed in a migration to 1.2 in 2024.11
|
|
||||||
CONF_DEVICES: [],
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,6 @@ class AnthropicOptionsFlow(OptionsFlow):
|
||||||
|
|
||||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||||
"""Initialize options flow."""
|
"""Initialize options flow."""
|
||||||
self.config_entry = config_entry
|
|
||||||
self.last_rendered_recommended = config_entry.options.get(
|
self.last_rendered_recommended = config_entry.options.get(
|
||||||
CONF_RECOMMENDED, False
|
CONF_RECOMMENDED, False
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,12 +15,14 @@ from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type AranetConfigEntry = ConfigEntry[
|
||||||
|
PassiveBluetoothProcessorCoordinator[Aranet4Advertisement]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _service_info_to_adv(
|
def _service_info_to_adv(
|
||||||
service_info: BluetoothServiceInfoBleak,
|
service_info: BluetoothServiceInfoBleak,
|
||||||
|
@ -28,30 +30,25 @@ def _service_info_to_adv(
|
||||||
return Aranet4Advertisement(service_info.device, service_info.advertisement)
|
return Aranet4Advertisement(service_info.device, service_info.advertisement)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AranetConfigEntry) -> bool:
|
||||||
"""Set up Aranet from a config entry."""
|
"""Set up Aranet from a config entry."""
|
||||||
|
|
||||||
address = entry.unique_id
|
address = entry.unique_id
|
||||||
assert address is not None
|
assert address is not None
|
||||||
coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
|
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||||
PassiveBluetoothProcessorCoordinator(
|
hass,
|
||||||
hass,
|
_LOGGER,
|
||||||
_LOGGER,
|
address=address,
|
||||||
address=address,
|
mode=BluetoothScanningMode.PASSIVE,
|
||||||
mode=BluetoothScanningMode.PASSIVE,
|
update_method=_service_info_to_adv,
|
||||||
update_method=_service_info_to_adv,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
entry.runtime_data = coordinator
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
entry.async_on_unload(
|
# only start after all platforms have had a chance to subscribe
|
||||||
coordinator.async_start()
|
entry.async_on_unload(coordinator.async_start())
|
||||||
) # only start after all platforms have had a chance to subscribe
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: AranetConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
|
|
@ -8,12 +8,10 @@ from typing import Any
|
||||||
from aranet4.client import Aranet4Advertisement
|
from aranet4.client import Aranet4Advertisement
|
||||||
from bleak.backends.device import BLEDevice
|
from bleak.backends.device import BLEDevice
|
||||||
|
|
||||||
from homeassistant import config_entries
|
|
||||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||||
PassiveBluetoothDataProcessor,
|
PassiveBluetoothDataProcessor,
|
||||||
PassiveBluetoothDataUpdate,
|
PassiveBluetoothDataUpdate,
|
||||||
PassiveBluetoothEntityKey,
|
PassiveBluetoothEntityKey,
|
||||||
PassiveBluetoothProcessorCoordinator,
|
|
||||||
PassiveBluetoothProcessorEntity,
|
PassiveBluetoothProcessorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
|
@ -38,7 +36,8 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import ARANET_MANUFACTURER_NAME, DOMAIN
|
from . import AranetConfigEntry
|
||||||
|
from .const import ARANET_MANUFACTURER_NAME
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
@ -174,20 +173,17 @@ def sensor_update_to_bluetooth_data_update(
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: config_entries.ConfigEntry,
|
entry: AranetConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Aranet sensors."""
|
"""Set up the Aranet sensors."""
|
||||||
coordinator: PassiveBluetoothProcessorCoordinator[Aranet4Advertisement] = hass.data[
|
|
||||||
DOMAIN
|
|
||||||
][entry.entry_id]
|
|
||||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
processor.async_add_entities_listener(
|
processor.async_add_entities_listener(
|
||||||
Aranet4BluetoothSensorEntity, async_add_entities
|
Aranet4BluetoothSensorEntity, async_add_entities
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
entry.async_on_unload(entry.runtime_data.async_register_processor(processor))
|
||||||
|
|
||||||
|
|
||||||
class Aranet4BluetoothSensorEntity(
|
class Aranet4BluetoothSensorEntity(
|
||||||
|
|
|
@ -22,8 +22,8 @@ class EnhancedAudioChunk:
|
||||||
timestamp_ms: int
|
timestamp_ms: int
|
||||||
"""Timestamp relative to start of audio stream (milliseconds)"""
|
"""Timestamp relative to start of audio stream (milliseconds)"""
|
||||||
|
|
||||||
is_speech: bool | None
|
speech_probability: float | None
|
||||||
"""True if audio chunk likely contains speech, False if not, None if unknown"""
|
"""Probability that audio chunk contains speech (0-1), None if unknown"""
|
||||||
|
|
||||||
|
|
||||||
class AudioEnhancer(ABC):
|
class AudioEnhancer(ABC):
|
||||||
|
@ -70,27 +70,27 @@ class MicroVadSpeexEnhancer(AudioEnhancer):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.vad: MicroVad | None = None
|
self.vad: MicroVad | None = None
|
||||||
self.threshold = 0.5
|
|
||||||
|
|
||||||
if self.is_vad_enabled:
|
if self.is_vad_enabled:
|
||||||
self.vad = MicroVad()
|
self.vad = MicroVad()
|
||||||
_LOGGER.debug("Initialized microVAD with threshold=%s", self.threshold)
|
_LOGGER.debug("Initialized microVAD")
|
||||||
|
|
||||||
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
|
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
|
||||||
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||||
is_speech: bool | None = None
|
speech_probability: float | None = None
|
||||||
|
|
||||||
assert len(audio) == BYTES_PER_CHUNK
|
assert len(audio) == BYTES_PER_CHUNK
|
||||||
|
|
||||||
if self.vad is not None:
|
if self.vad is not None:
|
||||||
# Run VAD
|
# Run VAD
|
||||||
speech_prob = self.vad.Process10ms(audio)
|
speech_probability = self.vad.Process10ms(audio)
|
||||||
is_speech = speech_prob > self.threshold
|
|
||||||
|
|
||||||
if self.audio_processor is not None:
|
if self.audio_processor is not None:
|
||||||
# Run noise suppression and auto gain
|
# Run noise suppression and auto gain
|
||||||
audio = self.audio_processor.Process10ms(audio).audio
|
audio = self.audio_processor.Process10ms(audio).audio
|
||||||
|
|
||||||
return EnhancedAudioChunk(
|
return EnhancedAudioChunk(
|
||||||
audio=audio, timestamp_ms=timestamp_ms, is_speech=is_speech
|
audio=audio,
|
||||||
|
timestamp_ms=timestamp_ms,
|
||||||
|
speech_probability=speech_probability,
|
||||||
)
|
)
|
||||||
|
|
|
@ -780,7 +780,9 @@ class PipelineRun:
|
||||||
# speaking the voice command.
|
# speaking the voice command.
|
||||||
audio_chunks_for_stt.extend(
|
audio_chunks_for_stt.extend(
|
||||||
EnhancedAudioChunk(
|
EnhancedAudioChunk(
|
||||||
audio=chunk_ts[0], timestamp_ms=chunk_ts[1], is_speech=False
|
audio=chunk_ts[0],
|
||||||
|
timestamp_ms=chunk_ts[1],
|
||||||
|
speech_probability=None,
|
||||||
)
|
)
|
||||||
for chunk_ts in result.queued_audio
|
for chunk_ts in result.queued_audio
|
||||||
)
|
)
|
||||||
|
@ -827,7 +829,7 @@ class PipelineRun:
|
||||||
|
|
||||||
if wake_word_vad is not None:
|
if wake_word_vad is not None:
|
||||||
chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate
|
chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate
|
||||||
if not wake_word_vad.process(chunk_seconds, chunk.is_speech):
|
if not wake_word_vad.process(chunk_seconds, chunk.speech_probability):
|
||||||
raise WakeWordTimeoutError(
|
raise WakeWordTimeoutError(
|
||||||
code="wake-word-timeout", message="Wake word was not detected"
|
code="wake-word-timeout", message="Wake word was not detected"
|
||||||
)
|
)
|
||||||
|
@ -955,7 +957,7 @@ class PipelineRun:
|
||||||
|
|
||||||
if stt_vad is not None:
|
if stt_vad is not None:
|
||||||
chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate
|
chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate
|
||||||
if not stt_vad.process(chunk_seconds, chunk.is_speech):
|
if not stt_vad.process(chunk_seconds, chunk.speech_probability):
|
||||||
# Silence detected at the end of voice command
|
# Silence detected at the end of voice command
|
||||||
self.process_event(
|
self.process_event(
|
||||||
PipelineEvent(
|
PipelineEvent(
|
||||||
|
@ -1221,7 +1223,7 @@ class PipelineRun:
|
||||||
yield EnhancedAudioChunk(
|
yield EnhancedAudioChunk(
|
||||||
audio=sub_chunk,
|
audio=sub_chunk,
|
||||||
timestamp_ms=timestamp_ms,
|
timestamp_ms=timestamp_ms,
|
||||||
is_speech=None, # no VAD
|
speech_probability=None, # no VAD
|
||||||
)
|
)
|
||||||
timestamp_ms += MS_PER_CHUNK
|
timestamp_ms += MS_PER_CHUNK
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ class AudioBuffer:
|
||||||
class VoiceCommandSegmenter:
|
class VoiceCommandSegmenter:
|
||||||
"""Segments an audio stream into voice commands."""
|
"""Segments an audio stream into voice commands."""
|
||||||
|
|
||||||
speech_seconds: float = 0.3
|
speech_seconds: float = 0.1
|
||||||
"""Seconds of speech before voice command has started."""
|
"""Seconds of speech before voice command has started."""
|
||||||
|
|
||||||
command_seconds: float = 1.0
|
command_seconds: float = 1.0
|
||||||
|
@ -96,6 +96,12 @@ class VoiceCommandSegmenter:
|
||||||
timed_out: bool = False
|
timed_out: bool = False
|
||||||
"""True a timeout occurred during voice command."""
|
"""True a timeout occurred during voice command."""
|
||||||
|
|
||||||
|
before_command_speech_threshold: float = 0.2
|
||||||
|
"""Probability threshold for speech before voice command."""
|
||||||
|
|
||||||
|
in_command_speech_threshold: float = 0.5
|
||||||
|
"""Probability threshold for speech during voice command."""
|
||||||
|
|
||||||
_speech_seconds_left: float = 0.0
|
_speech_seconds_left: float = 0.0
|
||||||
"""Seconds left before considering voice command as started."""
|
"""Seconds left before considering voice command as started."""
|
||||||
|
|
||||||
|
@ -124,7 +130,7 @@ class VoiceCommandSegmenter:
|
||||||
self._reset_seconds_left = self.reset_seconds
|
self._reset_seconds_left = self.reset_seconds
|
||||||
self.in_command = False
|
self.in_command = False
|
||||||
|
|
||||||
def process(self, chunk_seconds: float, is_speech: bool | None) -> bool:
|
def process(self, chunk_seconds: float, speech_probability: float | None) -> bool:
|
||||||
"""Process samples using external VAD.
|
"""Process samples using external VAD.
|
||||||
|
|
||||||
Returns False when command is done.
|
Returns False when command is done.
|
||||||
|
@ -142,7 +148,12 @@ class VoiceCommandSegmenter:
|
||||||
self.timed_out = True
|
self.timed_out = True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if speech_probability is None:
|
||||||
|
speech_probability = 0.0
|
||||||
|
|
||||||
if not self.in_command:
|
if not self.in_command:
|
||||||
|
# Before command
|
||||||
|
is_speech = speech_probability > self.before_command_speech_threshold
|
||||||
if is_speech:
|
if is_speech:
|
||||||
self._reset_seconds_left = self.reset_seconds
|
self._reset_seconds_left = self.reset_seconds
|
||||||
self._speech_seconds_left -= chunk_seconds
|
self._speech_seconds_left -= chunk_seconds
|
||||||
|
@ -160,24 +171,29 @@ class VoiceCommandSegmenter:
|
||||||
if self._reset_seconds_left <= 0:
|
if self._reset_seconds_left <= 0:
|
||||||
self._speech_seconds_left = self.speech_seconds
|
self._speech_seconds_left = self.speech_seconds
|
||||||
self._reset_seconds_left = self.reset_seconds
|
self._reset_seconds_left = self.reset_seconds
|
||||||
elif not is_speech:
|
|
||||||
# Silence in command
|
|
||||||
self._reset_seconds_left = self.reset_seconds
|
|
||||||
self._silence_seconds_left -= chunk_seconds
|
|
||||||
self._command_seconds_left -= chunk_seconds
|
|
||||||
if (self._silence_seconds_left <= 0) and (self._command_seconds_left <= 0):
|
|
||||||
# Command finished successfully
|
|
||||||
self.reset()
|
|
||||||
_LOGGER.debug("Voice command finished")
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
# Speech in command.
|
# In command
|
||||||
# Reset silence counter if enough speech.
|
is_speech = speech_probability > self.in_command_speech_threshold
|
||||||
self._reset_seconds_left -= chunk_seconds
|
if not is_speech:
|
||||||
self._command_seconds_left -= chunk_seconds
|
# Silence in command
|
||||||
if self._reset_seconds_left <= 0:
|
|
||||||
self._silence_seconds_left = self.silence_seconds
|
|
||||||
self._reset_seconds_left = self.reset_seconds
|
self._reset_seconds_left = self.reset_seconds
|
||||||
|
self._silence_seconds_left -= chunk_seconds
|
||||||
|
self._command_seconds_left -= chunk_seconds
|
||||||
|
if (self._silence_seconds_left <= 0) and (
|
||||||
|
self._command_seconds_left <= 0
|
||||||
|
):
|
||||||
|
# Command finished successfully
|
||||||
|
self.reset()
|
||||||
|
_LOGGER.debug("Voice command finished")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# Speech in command.
|
||||||
|
# Reset silence counter if enough speech.
|
||||||
|
self._reset_seconds_left -= chunk_seconds
|
||||||
|
self._command_seconds_left -= chunk_seconds
|
||||||
|
if self._reset_seconds_left <= 0:
|
||||||
|
self._silence_seconds_left = self.silence_seconds
|
||||||
|
self._reset_seconds_left = self.reset_seconds
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -226,6 +242,9 @@ class VoiceActivityTimeout:
|
||||||
reset_seconds: float = 0.5
|
reset_seconds: float = 0.5
|
||||||
"""Seconds of speech before resetting timeout."""
|
"""Seconds of speech before resetting timeout."""
|
||||||
|
|
||||||
|
speech_threshold: float = 0.5
|
||||||
|
"""Threshold for speech."""
|
||||||
|
|
||||||
_silence_seconds_left: float = 0.0
|
_silence_seconds_left: float = 0.0
|
||||||
"""Seconds left before considering voice command as stopped."""
|
"""Seconds left before considering voice command as stopped."""
|
||||||
|
|
||||||
|
@ -241,12 +260,15 @@ class VoiceActivityTimeout:
|
||||||
self._silence_seconds_left = self.silence_seconds
|
self._silence_seconds_left = self.silence_seconds
|
||||||
self._reset_seconds_left = self.reset_seconds
|
self._reset_seconds_left = self.reset_seconds
|
||||||
|
|
||||||
def process(self, chunk_seconds: float, is_speech: bool | None) -> bool:
|
def process(self, chunk_seconds: float, speech_probability: float | None) -> bool:
|
||||||
"""Process samples using external VAD.
|
"""Process samples using external VAD.
|
||||||
|
|
||||||
Returns False when timeout is reached.
|
Returns False when timeout is reached.
|
||||||
"""
|
"""
|
||||||
if is_speech:
|
if speech_probability is None:
|
||||||
|
speech_probability = 0.0
|
||||||
|
|
||||||
|
if speech_probability > self.speech_threshold:
|
||||||
# Speech
|
# Speech
|
||||||
self._reset_seconds_left -= chunk_seconds
|
self._reset_seconds_left -= chunk_seconds
|
||||||
if self._reset_seconds_left <= 0:
|
if self._reset_seconds_left <= 0:
|
||||||
|
|
|
@ -5,5 +5,5 @@
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/autarco",
|
"documentation": "https://www.home-assistant.io/integrations/autarco",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["autarco==3.0.0"]
|
"requirements": ["autarco==3.1.0"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,6 @@
|
||||||
"name": "Awair",
|
"name": "Awair",
|
||||||
"codeowners": ["@ahayworth", "@danielsjf"],
|
"codeowners": ["@ahayworth", "@danielsjf"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dhcp": [
|
|
||||||
{
|
|
||||||
"macaddress": "70886B1*"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/awair",
|
"documentation": "https://www.home-assistant.io/integrations/awair",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["python_awair"],
|
"loggers": ["python_awair"],
|
||||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.config_entries import (
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
OptionsFlowWithConfigEntry,
|
OptionsFlow,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
|
@ -59,9 +59,11 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(config_entry: ConfigEntry) -> AxisOptionsFlowHandler:
|
def async_get_options_flow(
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
) -> AxisOptionsFlowHandler:
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return AxisOptionsFlowHandler(config_entry)
|
return AxisOptionsFlowHandler()
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize the Axis config flow."""
|
"""Initialize the Axis config flow."""
|
||||||
|
@ -264,7 +266,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||||
return await self.async_step_user()
|
return await self.async_step_user()
|
||||||
|
|
||||||
|
|
||||||
class AxisOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
class AxisOptionsFlowHandler(OptionsFlow):
|
||||||
"""Handle Axis device options."""
|
"""Handle Axis device options."""
|
||||||
|
|
||||||
config_entry: AxisConfigEntry
|
config_entry: AxisConfigEntry
|
||||||
|
@ -282,8 +284,7 @@ class AxisOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Manage the Axis device stream options."""
|
"""Manage the Axis device stream options."""
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self.options.update(user_input)
|
return self.async_create_entry(data=self.config_entry.options | user_input)
|
||||||
return self.async_create_entry(title="", data=self.options)
|
|
||||||
|
|
||||||
schema = {}
|
schema = {}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["axis"],
|
"loggers": ["axis"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["axis==62"],
|
"requirements": ["axis==63"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "AXIS"
|
"manufacturer": "AXIS"
|
||||||
|
|
|
@ -124,7 +124,9 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
step_id=STEP_CONN_STRING,
|
step_id=STEP_CONN_STRING,
|
||||||
data_schema=CONN_STRING_SCHEMA,
|
data_schema=CONN_STRING_SCHEMA,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders=self._data[CONF_EVENT_HUB_INSTANCE_NAME],
|
description_placeholders={
|
||||||
|
"event_hub_instance_name": self._data[CONF_EVENT_HUB_INSTANCE_NAME]
|
||||||
|
},
|
||||||
last_step=True,
|
last_step=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -144,7 +146,9 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
step_id=STEP_SAS,
|
step_id=STEP_SAS,
|
||||||
data_schema=SAS_SCHEMA,
|
data_schema=SAS_SCHEMA,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders=self._data[CONF_EVENT_HUB_INSTANCE_NAME],
|
description_placeholders={
|
||||||
|
"event_hub_instance_name": self._data[CONF_EVENT_HUB_INSTANCE_NAME]
|
||||||
|
},
|
||||||
last_step=True,
|
last_step=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"""The Backup integration."""
|
"""The Backup integration."""
|
||||||
|
|
||||||
from homeassistant.components.hassio import is_hassio
|
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import DATA_MANAGER, DOMAIN, LOGGER
|
from .const import DATA_MANAGER, DOMAIN, LOGGER
|
||||||
|
@ -32,7 +32,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
|
||||||
async def async_handle_create_service(call: ServiceCall) -> None:
|
async def async_handle_create_service(call: ServiceCall) -> None:
|
||||||
"""Service handler for creating backups."""
|
"""Service handler for creating backups."""
|
||||||
await backup_manager.async_create_backup()
|
await backup_manager.async_create_backup(on_progress=None)
|
||||||
|
if backup_task := backup_manager.backup_task:
|
||||||
|
await backup_task
|
||||||
|
|
||||||
hass.services.async_register(DOMAIN, "create", async_handle_create_service)
|
hass.services.async_register(DOMAIN, "create", async_handle_create_service)
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ LOGGER = getLogger(__package__)
|
||||||
EXCLUDE_FROM_BACKUP = [
|
EXCLUDE_FROM_BACKUP = [
|
||||||
"__pycache__/*",
|
"__pycache__/*",
|
||||||
".DS_Store",
|
".DS_Store",
|
||||||
|
".HA_RESTORE",
|
||||||
"*.db-shm",
|
"*.db-shm",
|
||||||
"*.log.*",
|
"*.log.*",
|
||||||
"*.log",
|
"*.log",
|
||||||
|
|
|
@ -2,23 +2,26 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from aiohttp import BodyPartReader
|
||||||
from aiohttp.hdrs import CONTENT_DISPOSITION
|
from aiohttp.hdrs import CONTENT_DISPOSITION
|
||||||
from aiohttp.web import FileResponse, Request, Response
|
from aiohttp.web import FileResponse, Request, Response
|
||||||
|
|
||||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DATA_MANAGER
|
||||||
from .manager import BaseBackupManager
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_register_http_views(hass: HomeAssistant) -> None:
|
def async_register_http_views(hass: HomeAssistant) -> None:
|
||||||
"""Register the http views."""
|
"""Register the http views."""
|
||||||
hass.http.register_view(DownloadBackupView)
|
hass.http.register_view(DownloadBackupView)
|
||||||
|
hass.http.register_view(UploadBackupView)
|
||||||
|
|
||||||
|
|
||||||
class DownloadBackupView(HomeAssistantView):
|
class DownloadBackupView(HomeAssistantView):
|
||||||
|
@ -36,7 +39,7 @@ class DownloadBackupView(HomeAssistantView):
|
||||||
if not request["hass_user"].is_admin:
|
if not request["hass_user"].is_admin:
|
||||||
return Response(status=HTTPStatus.UNAUTHORIZED)
|
return Response(status=HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
manager: BaseBackupManager = request.app[KEY_HASS].data[DOMAIN]
|
manager = request.app[KEY_HASS].data[DATA_MANAGER]
|
||||||
backup = await manager.async_get_backup(slug=slug)
|
backup = await manager.async_get_backup(slug=slug)
|
||||||
|
|
||||||
if backup is None or not backup.path.exists():
|
if backup is None or not backup.path.exists():
|
||||||
|
@ -48,3 +51,29 @@ class DownloadBackupView(HomeAssistantView):
|
||||||
CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar"
|
CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UploadBackupView(HomeAssistantView):
|
||||||
|
"""Generate backup view."""
|
||||||
|
|
||||||
|
url = "/api/backup/upload"
|
||||||
|
name = "api:backup:upload"
|
||||||
|
|
||||||
|
@require_admin
|
||||||
|
async def post(self, request: Request) -> Response:
|
||||||
|
"""Upload a backup file."""
|
||||||
|
manager = request.app[KEY_HASS].data[DATA_MANAGER]
|
||||||
|
reader = await request.multipart()
|
||||||
|
contents = cast(BodyPartReader, await reader.next())
|
||||||
|
|
||||||
|
try:
|
||||||
|
await manager.async_receive_backup(contents=contents)
|
||||||
|
except OSError as err:
|
||||||
|
return Response(
|
||||||
|
body=f"Can't write backup file {err}",
|
||||||
|
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
return Response(status=HTTPStatus.CREATED)
|
||||||
|
|
|
@ -4,18 +4,24 @@ from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from queue import SimpleQueue
|
||||||
|
import shutil
|
||||||
import tarfile
|
import tarfile
|
||||||
from tarfile import TarError
|
from tarfile import TarError
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
import time
|
import time
|
||||||
from typing import Any, Protocol, cast
|
from typing import Any, Protocol, cast
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
from securetar import SecureTarFile, atomic_contents_add
|
from securetar import SecureTarFile, atomic_contents_add
|
||||||
|
|
||||||
|
from homeassistant.backup_restore import RESTORE_BACKUP_FILE
|
||||||
from homeassistant.const import __version__ as HAVERSION
|
from homeassistant.const import __version__ as HAVERSION
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
@ -29,6 +35,13 @@ from .const import DOMAIN, EXCLUDE_FROM_BACKUP, LOGGER
|
||||||
BUF_SIZE = 2**20 * 4 # 4MB
|
BUF_SIZE = 2**20 * 4 # 4MB
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class NewBackup:
|
||||||
|
"""New backup class."""
|
||||||
|
|
||||||
|
slug: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class Backup:
|
class Backup:
|
||||||
"""Backup class."""
|
"""Backup class."""
|
||||||
|
@ -44,6 +57,15 @@ class Backup:
|
||||||
return {**asdict(self), "path": self.path.as_posix()}
|
return {**asdict(self), "path": self.path.as_posix()}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class BackupProgress:
|
||||||
|
"""Backup progress class."""
|
||||||
|
|
||||||
|
done: bool
|
||||||
|
stage: str | None
|
||||||
|
success: bool | None
|
||||||
|
|
||||||
|
|
||||||
class BackupPlatformProtocol(Protocol):
|
class BackupPlatformProtocol(Protocol):
|
||||||
"""Define the format that backup platforms can have."""
|
"""Define the format that backup platforms can have."""
|
||||||
|
|
||||||
|
@ -60,7 +82,7 @@ class BaseBackupManager(abc.ABC):
|
||||||
def __init__(self, hass: HomeAssistant) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize the backup manager."""
|
"""Initialize the backup manager."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.backing_up = False
|
self.backup_task: asyncio.Task | None = None
|
||||||
self.backups: dict[str, Backup] = {}
|
self.backups: dict[str, Backup] = {}
|
||||||
self.loaded_platforms = False
|
self.loaded_platforms = False
|
||||||
self.platforms: dict[str, BackupPlatformProtocol] = {}
|
self.platforms: dict[str, BackupPlatformProtocol] = {}
|
||||||
|
@ -124,7 +146,16 @@ class BaseBackupManager(abc.ABC):
|
||||||
self.loaded_platforms = True
|
self.loaded_platforms = True
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def async_create_backup(self, **kwargs: Any) -> Backup:
|
async def async_restore_backup(self, slug: str, **kwargs: Any) -> None:
|
||||||
|
"""Restore a backup."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def async_create_backup(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
on_progress: Callable[[BackupProgress], None] | None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> NewBackup:
|
||||||
"""Generate a backup."""
|
"""Generate a backup."""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -142,6 +173,15 @@ class BaseBackupManager(abc.ABC):
|
||||||
async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None:
|
async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None:
|
||||||
"""Remove a backup."""
|
"""Remove a backup."""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def async_receive_backup(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
contents: aiohttp.BodyPartReader,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Receive and store a backup file from upload."""
|
||||||
|
|
||||||
|
|
||||||
class BackupManager(BaseBackupManager):
|
class BackupManager(BaseBackupManager):
|
||||||
"""Backup manager for the Backup integration."""
|
"""Backup manager for the Backup integration."""
|
||||||
|
@ -217,17 +257,93 @@ class BackupManager(BaseBackupManager):
|
||||||
LOGGER.debug("Removed backup located at %s", backup.path)
|
LOGGER.debug("Removed backup located at %s", backup.path)
|
||||||
self.backups.pop(slug)
|
self.backups.pop(slug)
|
||||||
|
|
||||||
async def async_create_backup(self, **kwargs: Any) -> Backup:
|
async def async_receive_backup(
|
||||||
"""Generate a backup."""
|
self,
|
||||||
if self.backing_up:
|
*,
|
||||||
raise HomeAssistantError("Backup already in progress")
|
contents: aiohttp.BodyPartReader,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Receive and store a backup file from upload."""
|
||||||
|
queue: SimpleQueue[tuple[bytes, asyncio.Future[None] | None] | None] = (
|
||||||
|
SimpleQueue()
|
||||||
|
)
|
||||||
|
temp_dir_handler = await self.hass.async_add_executor_job(TemporaryDirectory)
|
||||||
|
target_temp_file = Path(
|
||||||
|
temp_dir_handler.name, contents.filename or "backup.tar"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _sync_queue_consumer() -> None:
|
||||||
|
with target_temp_file.open("wb") as file_handle:
|
||||||
|
while True:
|
||||||
|
if (_chunk_future := queue.get()) is None:
|
||||||
|
break
|
||||||
|
_chunk, _future = _chunk_future
|
||||||
|
if _future is not None:
|
||||||
|
self.hass.loop.call_soon_threadsafe(_future.set_result, None)
|
||||||
|
file_handle.write(_chunk)
|
||||||
|
|
||||||
|
fut: asyncio.Future[None] | None = None
|
||||||
|
try:
|
||||||
|
fut = self.hass.async_add_executor_job(_sync_queue_consumer)
|
||||||
|
megabytes_sending = 0
|
||||||
|
while chunk := await contents.read_chunk(BUF_SIZE):
|
||||||
|
megabytes_sending += 1
|
||||||
|
if megabytes_sending % 5 != 0:
|
||||||
|
queue.put_nowait((chunk, None))
|
||||||
|
continue
|
||||||
|
|
||||||
|
chunk_future = self.hass.loop.create_future()
|
||||||
|
queue.put_nowait((chunk, chunk_future))
|
||||||
|
await asyncio.wait(
|
||||||
|
(fut, chunk_future),
|
||||||
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
|
)
|
||||||
|
if fut.done():
|
||||||
|
# The executor job failed
|
||||||
|
break
|
||||||
|
|
||||||
|
queue.put_nowait(None) # terminate queue consumer
|
||||||
|
finally:
|
||||||
|
if fut is not None:
|
||||||
|
await fut
|
||||||
|
|
||||||
|
def _move_and_cleanup() -> None:
|
||||||
|
shutil.move(target_temp_file, self.backup_dir / target_temp_file.name)
|
||||||
|
temp_dir_handler.cleanup()
|
||||||
|
|
||||||
|
await self.hass.async_add_executor_job(_move_and_cleanup)
|
||||||
|
await self.load_backups()
|
||||||
|
|
||||||
|
async def async_create_backup(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
on_progress: Callable[[BackupProgress], None] | None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> NewBackup:
|
||||||
|
"""Generate a backup."""
|
||||||
|
if self.backup_task:
|
||||||
|
raise HomeAssistantError("Backup already in progress")
|
||||||
|
backup_name = f"Core {HAVERSION}"
|
||||||
|
date_str = dt_util.now().isoformat()
|
||||||
|
slug = _generate_slug(date_str, backup_name)
|
||||||
|
self.backup_task = self.hass.async_create_task(
|
||||||
|
self._async_create_backup(backup_name, date_str, slug, on_progress),
|
||||||
|
name="backup_manager_create_backup",
|
||||||
|
eager_start=False, # To ensure the task is not started before we return
|
||||||
|
)
|
||||||
|
return NewBackup(slug=slug)
|
||||||
|
|
||||||
|
async def _async_create_backup(
|
||||||
|
self,
|
||||||
|
backup_name: str,
|
||||||
|
date_str: str,
|
||||||
|
slug: str,
|
||||||
|
on_progress: Callable[[BackupProgress], None] | None,
|
||||||
|
) -> Backup:
|
||||||
|
"""Generate a backup."""
|
||||||
|
success = False
|
||||||
try:
|
try:
|
||||||
self.backing_up = True
|
|
||||||
await self.async_pre_backup_actions()
|
await self.async_pre_backup_actions()
|
||||||
backup_name = f"Core {HAVERSION}"
|
|
||||||
date_str = dt_util.now().isoformat()
|
|
||||||
slug = _generate_slug(date_str, backup_name)
|
|
||||||
|
|
||||||
backup_data = {
|
backup_data = {
|
||||||
"slug": slug,
|
"slug": slug,
|
||||||
|
@ -254,9 +370,12 @@ class BackupManager(BaseBackupManager):
|
||||||
if self.loaded_backups:
|
if self.loaded_backups:
|
||||||
self.backups[slug] = backup
|
self.backups[slug] = backup
|
||||||
LOGGER.debug("Generated new backup with slug %s", slug)
|
LOGGER.debug("Generated new backup with slug %s", slug)
|
||||||
|
success = True
|
||||||
return backup
|
return backup
|
||||||
finally:
|
finally:
|
||||||
self.backing_up = False
|
if on_progress:
|
||||||
|
on_progress(BackupProgress(done=True, stage=None, success=success))
|
||||||
|
self.backup_task = None
|
||||||
await self.async_post_backup_actions()
|
await self.async_post_backup_actions()
|
||||||
|
|
||||||
def _mkdir_and_generate_backup_contents(
|
def _mkdir_and_generate_backup_contents(
|
||||||
|
@ -291,6 +410,25 @@ class BackupManager(BaseBackupManager):
|
||||||
|
|
||||||
return tar_file_path.stat().st_size
|
return tar_file_path.stat().st_size
|
||||||
|
|
||||||
|
async def async_restore_backup(self, slug: str, **kwargs: Any) -> None:
|
||||||
|
"""Restore a backup.
|
||||||
|
|
||||||
|
This will write the restore information to .HA_RESTORE which
|
||||||
|
will be handled during startup by the restore_backup module.
|
||||||
|
"""
|
||||||
|
if (backup := await self.async_get_backup(slug=slug)) is None:
|
||||||
|
raise HomeAssistantError(f"Backup {slug} not found")
|
||||||
|
|
||||||
|
def _write_restore_file() -> None:
|
||||||
|
"""Write the restore file."""
|
||||||
|
Path(self.hass.config.path(RESTORE_BACKUP_FILE)).write_text(
|
||||||
|
json.dumps({"path": backup.path.as_posix()}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.hass.async_add_executor_job(_write_restore_file)
|
||||||
|
await self.hass.services.async_call("homeassistant", "restart", {})
|
||||||
|
|
||||||
|
|
||||||
def _generate_slug(date: str, name: str) -> str:
|
def _generate_slug(date: str, name: str) -> str:
|
||||||
"""Generate a backup slug."""
|
"""Generate a backup slug."""
|
||||||
|
|
|
@ -8,6 +8,7 @@ from homeassistant.components import websocket_api
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
from .const import DATA_MANAGER, LOGGER
|
from .const import DATA_MANAGER, LOGGER
|
||||||
|
from .manager import BackupProgress
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -22,6 +23,7 @@ def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) ->
|
||||||
websocket_api.async_register_command(hass, handle_info)
|
websocket_api.async_register_command(hass, handle_info)
|
||||||
websocket_api.async_register_command(hass, handle_create)
|
websocket_api.async_register_command(hass, handle_create)
|
||||||
websocket_api.async_register_command(hass, handle_remove)
|
websocket_api.async_register_command(hass, handle_remove)
|
||||||
|
websocket_api.async_register_command(hass, handle_restore)
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.require_admin
|
@websocket_api.require_admin
|
||||||
|
@ -39,7 +41,7 @@ async def handle_info(
|
||||||
msg["id"],
|
msg["id"],
|
||||||
{
|
{
|
||||||
"backups": list(backups.values()),
|
"backups": list(backups.values()),
|
||||||
"backing_up": manager.backing_up,
|
"backing_up": manager.backup_task is not None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -85,6 +87,24 @@ async def handle_remove(
|
||||||
connection.send_result(msg["id"])
|
connection.send_result(msg["id"])
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "backup/restore",
|
||||||
|
vol.Required("slug"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def handle_restore(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Restore a backup."""
|
||||||
|
await hass.data[DATA_MANAGER].async_restore_backup(msg["slug"])
|
||||||
|
connection.send_result(msg["id"])
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.require_admin
|
@websocket_api.require_admin
|
||||||
@websocket_api.websocket_command({vol.Required("type"): "backup/generate"})
|
@websocket_api.websocket_command({vol.Required("type"): "backup/generate"})
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
|
@ -94,7 +114,11 @@ async def handle_create(
|
||||||
msg: dict[str, Any],
|
msg: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Generate a backup."""
|
"""Generate a backup."""
|
||||||
backup = await hass.data[DATA_MANAGER].async_create_backup()
|
|
||||||
|
def on_progress(progress: BackupProgress) -> None:
|
||||||
|
connection.send_message(websocket_api.event_message(msg["id"], progress))
|
||||||
|
|
||||||
|
backup = await hass.data[DATA_MANAGER].async_create_backup(on_progress=on_progress)
|
||||||
connection.send_result(msg["id"], backup)
|
connection.send_result(msg["id"], backup)
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,7 +132,6 @@ async def handle_backup_start(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Backup start notification."""
|
"""Backup start notification."""
|
||||||
manager = hass.data[DATA_MANAGER]
|
manager = hass.data[DATA_MANAGER]
|
||||||
manager.backing_up = True
|
|
||||||
LOGGER.debug("Backup start notification")
|
LOGGER.debug("Backup start notification")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -130,7 +153,6 @@ async def handle_backup_end(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Backup end notification."""
|
"""Backup end notification."""
|
||||||
manager = hass.data[DATA_MANAGER]
|
manager = hass.data[DATA_MANAGER]
|
||||||
manager.backing_up = False
|
|
||||||
LOGGER.debug("Backup end notification")
|
LOGGER.debug("Backup end notification")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from .const import CONF_SYNC_TIME, DEFAULT_SYNC_TIME, DOMAIN
|
from .const import CONF_SYNC_TIME, DEFAULT_SYNC_TIME
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -30,8 +30,10 @@ PLATFORMS = [
|
||||||
KEEP_ALIVE_INTERVAL = timedelta(minutes=1)
|
KEEP_ALIVE_INTERVAL = timedelta(minutes=1)
|
||||||
SYNC_TIME_INTERVAL = timedelta(hours=1)
|
SYNC_TIME_INTERVAL = timedelta(hours=1)
|
||||||
|
|
||||||
|
type BalboaConfigEntry = ConfigEntry[SpaClient]
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: BalboaConfigEntry) -> bool:
|
||||||
"""Set up Balboa Spa from a config entry."""
|
"""Set up Balboa Spa from a config entry."""
|
||||||
host = entry.data[CONF_HOST]
|
host = entry.data[CONF_HOST]
|
||||||
|
|
||||||
|
@ -44,41 +46,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
_LOGGER.error("Failed to get spa info at %s", host)
|
_LOGGER.error("Failed to get spa info at %s", host)
|
||||||
raise ConfigEntryNotReady("Unable to configure")
|
raise ConfigEntryNotReady("Unable to configure")
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = spa
|
entry.runtime_data = spa
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
await async_setup_time_sync(hass, entry)
|
await async_setup_time_sync(hass, entry)
|
||||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||||
|
entry.async_on_unload(spa.disconnect)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: BalboaConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
_LOGGER.debug("Disconnecting from spa")
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
|
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
await spa.disconnect()
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
|
||||||
|
|
||||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
async def update_listener(hass: HomeAssistant, entry: BalboaConfigEntry) -> None:
|
||||||
"""Handle options update."""
|
"""Handle options update."""
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_time_sync(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
async def async_setup_time_sync(hass: HomeAssistant, entry: BalboaConfigEntry) -> None:
|
||||||
"""Set up the time sync."""
|
"""Set up the time sync."""
|
||||||
if not entry.options.get(CONF_SYNC_TIME, DEFAULT_SYNC_TIME):
|
if not entry.options.get(CONF_SYNC_TIME, DEFAULT_SYNC_TIME):
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.debug("Setting up daily time sync")
|
_LOGGER.debug("Setting up daily time sync")
|
||||||
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
|
spa = entry.runtime_data
|
||||||
|
|
||||||
async def sync_time(now: datetime) -> None:
|
async def sync_time(now: datetime) -> None:
|
||||||
now = dt_util.as_local(now)
|
now = dt_util.as_local(now)
|
||||||
|
|
|
@ -12,19 +12,20 @@ from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import BalboaConfigEntry
|
||||||
from .entity import BalboaEntity
|
from .entity import BalboaEntity
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: BalboaConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the spa's binary sensors."""
|
"""Set up the spa's binary sensors."""
|
||||||
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
|
spa = entry.runtime_data
|
||||||
entities = [
|
entities = [
|
||||||
BalboaBinarySensorEntity(spa, description)
|
BalboaBinarySensorEntity(spa, description)
|
||||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||||
|
|
|
@ -14,7 +14,6 @@ from homeassistant.components.climate import (
|
||||||
HVACAction,
|
HVACAction,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
PRECISION_HALVES,
|
PRECISION_HALVES,
|
||||||
|
@ -24,6 +23,7 @@ from homeassistant.const import (
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import BalboaConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import BalboaEntity
|
from .entity import BalboaEntity
|
||||||
|
|
||||||
|
@ -45,10 +45,12 @@ TEMPERATURE_UNIT_MAP = {
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: BalboaConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the spa climate entity."""
|
"""Set up the spa climate entity."""
|
||||||
async_add_entities([BalboaClimateEntity(hass.data[DOMAIN][entry.entry_id])])
|
async_add_entities([BalboaClimateEntity(entry.runtime_data)])
|
||||||
|
|
||||||
|
|
||||||
class BalboaClimateEntity(BalboaEntity, ClimateEntity):
|
class BalboaClimateEntity(BalboaEntity, ClimateEntity):
|
||||||
|
|
|
@ -5,11 +5,10 @@ from __future__ import annotations
|
||||||
import math
|
import math
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from pybalboa import SpaClient, SpaControl
|
from pybalboa import SpaControl
|
||||||
from pybalboa.enums import OffOnState, UnknownState
|
from pybalboa.enums import OffOnState, UnknownState
|
||||||
|
|
||||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util.percentage import (
|
from homeassistant.util.percentage import (
|
||||||
|
@ -17,15 +16,17 @@ from homeassistant.util.percentage import (
|
||||||
ranged_value_to_percentage,
|
ranged_value_to_percentage,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import BalboaConfigEntry
|
||||||
from .entity import BalboaEntity
|
from .entity import BalboaEntity
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: BalboaConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the spa's pumps."""
|
"""Set up the spa's pumps."""
|
||||||
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
|
spa = entry.runtime_data
|
||||||
async_add_entities(BalboaPumpFanEntity(control) for control in spa.pumps)
|
async_add_entities(BalboaPumpFanEntity(control) for control in spa.pumps)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,23 +4,24 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from pybalboa import SpaClient, SpaControl
|
from pybalboa import SpaControl
|
||||||
from pybalboa.enums import OffOnState, UnknownState
|
from pybalboa.enums import OffOnState, UnknownState
|
||||||
|
|
||||||
from homeassistant.components.light import ColorMode, LightEntity
|
from homeassistant.components.light import ColorMode, LightEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import BalboaConfigEntry
|
||||||
from .entity import BalboaEntity
|
from .entity import BalboaEntity
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: BalboaConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the spa's lights."""
|
"""Set up the spa's lights."""
|
||||||
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
|
spa = entry.runtime_data
|
||||||
async_add_entities(BalboaLightEntity(control) for control in spa.lights)
|
async_add_entities(BalboaLightEntity(control) for control in spa.lights)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
"""Support for Spa Client selects."""
|
"""Support for Spa Client selects."""
|
||||||
|
|
||||||
from pybalboa import SpaClient, SpaControl
|
from pybalboa import SpaControl
|
||||||
from pybalboa.enums import LowHighRange
|
from pybalboa.enums import LowHighRange
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity
|
from homeassistant.components.select import SelectEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from . import BalboaConfigEntry
|
||||||
from .entity import BalboaEntity
|
from .entity import BalboaEntity
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: BalboaConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the spa select entity."""
|
"""Set up the spa select entity."""
|
||||||
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
|
spa = entry.runtime_data
|
||||||
async_add_entities([BalboaTempRangeSelectEntity(spa.temperature_range)])
|
async_add_entities([BalboaTempRangeSelectEntity(spa.temperature_range)])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -31,10 +31,12 @@ class BangOlufsenData:
|
||||||
client: MozartClient
|
client: MozartClient
|
||||||
|
|
||||||
|
|
||||||
|
type BangOlufsenConfigEntry = ConfigEntry[BangOlufsenData]
|
||||||
|
|
||||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry) -> bool:
|
||||||
"""Set up from a config entry."""
|
"""Set up from a config entry."""
|
||||||
|
|
||||||
# Remove casts to str
|
# Remove casts to str
|
||||||
|
@ -67,10 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
websocket = BangOlufsenWebsocket(hass, entry, client)
|
websocket = BangOlufsenWebsocket(hass, entry, client)
|
||||||
|
|
||||||
# Add the websocket and API client
|
# Add the websocket and API client
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BangOlufsenData(
|
entry.runtime_data = BangOlufsenData(websocket, client)
|
||||||
websocket,
|
|
||||||
client,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Start WebSocket connection
|
# Start WebSocket connection
|
||||||
await client.connect_notifications(remote_control=True, reconnect=True)
|
await client.connect_notifications(remote_control=True, reconnect=True)
|
||||||
|
@ -80,15 +79,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: BangOlufsenConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
# Close the API client and WebSocket notification listener
|
# Close the API client and WebSocket notification listener
|
||||||
hass.data[DOMAIN][entry.entry_id].client.disconnect_notifications()
|
entry.runtime_data.client.disconnect_notifications()
|
||||||
await hass.data[DOMAIN][entry.entry_id].client.close_api_client()
|
await entry.runtime_data.client.close_api_client()
|
||||||
|
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
|
|
@ -7,20 +7,19 @@ from typing import Final
|
||||||
|
|
||||||
from mozart_api.models import Source, SourceArray, SourceTypeEnum
|
from mozart_api.models import Source, SourceArray, SourceTypeEnum
|
||||||
|
|
||||||
from homeassistant.components.media_player import MediaPlayerState, MediaType
|
from homeassistant.components.media_player import (
|
||||||
|
MediaPlayerState,
|
||||||
|
MediaType,
|
||||||
|
RepeatMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BangOlufsenSource:
|
class BangOlufsenSource:
|
||||||
"""Class used for associating device source ids with friendly names. May not include all sources."""
|
"""Class used for associating device source ids with friendly names. May not include all sources."""
|
||||||
|
|
||||||
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")
|
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
|
||||||
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
|
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
|
||||||
NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
|
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
|
||||||
DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
|
|
||||||
TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
|
|
||||||
|
|
||||||
|
|
||||||
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
||||||
|
@ -36,6 +35,17 @@ BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
||||||
"unknown": MediaPlayerState.IDLE,
|
"unknown": MediaPlayerState.IDLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Dict used for translating Home Assistant settings to device repeat settings.
|
||||||
|
BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
|
||||||
|
RepeatMode.ALL: "all",
|
||||||
|
RepeatMode.ONE: "track",
|
||||||
|
RepeatMode.OFF: "none",
|
||||||
|
}
|
||||||
|
# Dict used for translating device repeat settings to Home Assistant settings.
|
||||||
|
BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
|
||||||
|
value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Media types for play_media
|
# Media types for play_media
|
||||||
class BangOlufsenMediaType(StrEnum):
|
class BangOlufsenMediaType(StrEnum):
|
||||||
|
@ -123,20 +133,6 @@ VALID_MEDIA_TYPES: Final[tuple] = (
|
||||||
MediaType.CHANNEL,
|
MediaType.CHANNEL,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sources on the device that should not be selectable by the user
|
|
||||||
HIDDEN_SOURCE_IDS: Final[tuple] = (
|
|
||||||
"airPlay",
|
|
||||||
"bluetooth",
|
|
||||||
"chromeCast",
|
|
||||||
"generator",
|
|
||||||
"local",
|
|
||||||
"dlna",
|
|
||||||
"qplay",
|
|
||||||
"wpl",
|
|
||||||
"pl",
|
|
||||||
"beolink",
|
|
||||||
"usbIn",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Fallback sources to use in case of API failure.
|
# Fallback sources to use in case of API failure.
|
||||||
FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||||
|
@ -144,23 +140,26 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||||
Source(
|
Source(
|
||||||
id="uriStreamer",
|
id="uriStreamer",
|
||||||
is_enabled=True,
|
is_enabled=True,
|
||||||
is_playable=False,
|
is_playable=True,
|
||||||
name="Audio Streamer",
|
name="Audio Streamer",
|
||||||
type=SourceTypeEnum(value="uriStreamer"),
|
type=SourceTypeEnum(value="uriStreamer"),
|
||||||
|
is_seekable=False,
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="bluetooth",
|
id="bluetooth",
|
||||||
is_enabled=True,
|
is_enabled=True,
|
||||||
is_playable=False,
|
is_playable=True,
|
||||||
name="Bluetooth",
|
name="Bluetooth",
|
||||||
type=SourceTypeEnum(value="bluetooth"),
|
type=SourceTypeEnum(value="bluetooth"),
|
||||||
|
is_seekable=False,
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="spotify",
|
id="spotify",
|
||||||
is_enabled=True,
|
is_enabled=True,
|
||||||
is_playable=False,
|
is_playable=True,
|
||||||
name="Spotify Connect",
|
name="Spotify Connect",
|
||||||
type=SourceTypeEnum(value="spotify"),
|
type=SourceTypeEnum(value="spotify"),
|
||||||
|
is_seekable=True,
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="lineIn",
|
id="lineIn",
|
||||||
|
@ -168,6 +167,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||||
is_playable=True,
|
is_playable=True,
|
||||||
name="Line-In",
|
name="Line-In",
|
||||||
type=SourceTypeEnum(value="lineIn"),
|
type=SourceTypeEnum(value="lineIn"),
|
||||||
|
is_seekable=False,
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="spdif",
|
id="spdif",
|
||||||
|
@ -175,6 +175,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||||
is_playable=True,
|
is_playable=True,
|
||||||
name="Optical",
|
name="Optical",
|
||||||
type=SourceTypeEnum(value="spdif"),
|
type=SourceTypeEnum(value="spdif"),
|
||||||
|
is_seekable=False,
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="netRadio",
|
id="netRadio",
|
||||||
|
@ -182,6 +183,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||||
is_playable=True,
|
is_playable=True,
|
||||||
name="B&O Radio",
|
name="B&O Radio",
|
||||||
type=SourceTypeEnum(value="netRadio"),
|
type=SourceTypeEnum(value="netRadio"),
|
||||||
|
is_seekable=False,
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="deezer",
|
id="deezer",
|
||||||
|
@ -189,6 +191,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||||
is_playable=True,
|
is_playable=True,
|
||||||
name="Deezer",
|
name="Deezer",
|
||||||
type=SourceTypeEnum(value="deezer"),
|
type=SourceTypeEnum(value="deezer"),
|
||||||
|
is_seekable=True,
|
||||||
),
|
),
|
||||||
Source(
|
Source(
|
||||||
id="tidalConnect",
|
id="tidalConnect",
|
||||||
|
@ -196,6 +199,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||||
is_playable=True,
|
is_playable=True,
|
||||||
name="Tidal Connect",
|
name="Tidal Connect",
|
||||||
type=SourceTypeEnum(value="tidalConnect"),
|
type=SourceTypeEnum(value="tidalConnect"),
|
||||||
|
is_seekable=True,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
9
homeassistant/components/bang_olufsen/icons.json
Normal file
9
homeassistant/components/bang_olufsen/icons.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"services": {
|
||||||
|
"beolink_join": { "service": "mdi:location-enter" },
|
||||||
|
"beolink_expand": { "service": "mdi:location-enter" },
|
||||||
|
"beolink_unexpand": { "service": "mdi:location-exit" },
|
||||||
|
"beolink_leave": { "service": "mdi:close-circle-outline" },
|
||||||
|
"beolink_allstandby": { "service": "mdi:close-circle-multiple-outline" }
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue