mirror of
https://github.com/home-assistant/core.git
synced 2026-01-06 23:57:17 +01:00
Compare commits
574 Commits
copilot-in
...
2025.7.0b9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fc3fa51a8 | ||
|
|
4eb688b560 | ||
|
|
9472ff5d36 | ||
|
|
12e8b81ec7 | ||
|
|
ec5e543c09 | ||
|
|
116c745872 | ||
|
|
1fdf152292 | ||
|
|
b816f1a408 | ||
|
|
eb351e6505 | ||
|
|
2f27d55495 | ||
|
|
fa1bed1849 | ||
|
|
b8c19f23f3 | ||
|
|
b677ce6c90 | ||
|
|
0e6bbb30c1 | ||
|
|
fdba791f18 | ||
|
|
d4dec6c7a9 | ||
|
|
f838e85a79 | ||
|
|
04ae966544 | ||
|
|
b2c393db72 | ||
|
|
3ed440a3af | ||
|
|
01e7efc7b4 | ||
|
|
60a930554a | ||
|
|
c707bf6264 | ||
|
|
3548ab70fd | ||
|
|
e272ab1885 | ||
|
|
d5d1b620d0 | ||
|
|
8b2f4f0f86 | ||
|
|
725269ecda | ||
|
|
c42fc818bf | ||
|
|
5554e38171 | ||
|
|
b25acfe823 | ||
|
|
ff25948e37 | ||
|
|
f85fc7173f | ||
|
|
748cc6386d | ||
|
|
47b232db49 | ||
|
|
c61935fc41 | ||
|
|
414318f3fb | ||
|
|
08985d783f | ||
|
|
e4bcde7d20 | ||
|
|
db04c77e62 | ||
|
|
e8204e5f8e | ||
|
|
66cf9c4ed5 | ||
|
|
1f6d28dcbf | ||
|
|
328e838351 | ||
|
|
62a1c8af11 | ||
|
|
b50e599517 | ||
|
|
3c7c9176d2 | ||
|
|
c771f5fe1e | ||
|
|
6dc464ad73 | ||
|
|
ae48e3716e | ||
|
|
1543726095 | ||
|
|
adbace95c3 | ||
|
|
578b43cf61 | ||
|
|
a8b5d1511d | ||
|
|
5a0a1bbbf4 | ||
|
|
cf2e69ed74 | ||
|
|
c32b44b774 | ||
|
|
2f69ed4a8a | ||
|
|
4b3449fe0c | ||
|
|
33e1c6de68 | ||
|
|
81e712ea49 | ||
|
|
d3c5684cd0 | ||
|
|
862b7460b5 | ||
|
|
a65eb57539 | ||
|
|
b537850f52 | ||
|
|
16c6bd08f8 | ||
|
|
18834849c2 | ||
|
|
e4d820799f | ||
|
|
013a35176a | ||
|
|
8230557aef | ||
|
|
5451063714 | ||
|
|
8cdc7523a4 | ||
|
|
77ccfbd3a9 | ||
|
|
4977ee4998 | ||
|
|
5c0f2d37f0 | ||
|
|
0b5d2ab8e4 | ||
|
|
47f3bf29dd | ||
|
|
62f7cbb51e | ||
|
|
b9e2c5d34c | ||
|
|
1829acd0e1 | ||
|
|
41b9a7a9a3 | ||
|
|
9782637ec8 | ||
|
|
6bd6fa65d2 | ||
|
|
85343a9f53 | ||
|
|
bc607dd013 | ||
|
|
c2c388e0cc | ||
|
|
3fc154e1d7 | ||
|
|
efb29d024e | ||
|
|
263823c92c | ||
|
|
e5e6ed601b | ||
|
|
28dfc997f3 | ||
|
|
f93ab8d519 | ||
|
|
cb359da79e | ||
|
|
6a7385590a | ||
|
|
c0ec987b07 | ||
|
|
26521f8cc0 | ||
|
|
4df1f702bf | ||
|
|
c8422c9fb8 | ||
|
|
f8207a2e0e | ||
|
|
9cc75f3458 | ||
|
|
a233b6b1e3 | ||
|
|
c7677b91da | ||
|
|
1f57bba9cd | ||
|
|
4cc10ca2e2 | ||
|
|
153e1e43e8 | ||
|
|
398dd3ae46 | ||
|
|
17fd850fa6 | ||
|
|
ae062b230c | ||
|
|
d523f85404 | ||
|
|
f28d6582c6 | ||
|
|
1e81e5990e | ||
|
|
5fe2e4b6ed | ||
|
|
914bb3aa76 | ||
|
|
cfa6746115 | ||
|
|
03f9caf3eb | ||
|
|
6b2aaf3fdb | ||
|
|
2c4ea0d584 | ||
|
|
e627811f7a | ||
|
|
150f41641b | ||
|
|
b9a7371996 | ||
|
|
7d0e99da43 | ||
|
|
71f281cc14 | ||
|
|
aec812a475 | ||
|
|
d4b548b169 | ||
|
|
a296324c30 | ||
|
|
cff3d3d6ac | ||
|
|
26e3caea9a | ||
|
|
2b5f5f641d | ||
|
|
99079d2980 | ||
|
|
2800921a5d | ||
|
|
3268b9ee18 | ||
|
|
02c3cdd5d4 | ||
|
|
f34f17bc24 | ||
|
|
1fb587bf03 | ||
|
|
d8258924f7 | ||
|
|
c05d8aab1c | ||
|
|
e210681751 | ||
|
|
809aced9cc | ||
|
|
977e8adbfb | ||
|
|
c54ce7eabd | ||
|
|
c5f8acfe93 | ||
|
|
8393f17bb3 | ||
|
|
8918b0d7a9 | ||
|
|
c447729ce4 | ||
|
|
12812049ea | ||
|
|
47811e13a6 | ||
|
|
7587fc985f | ||
|
|
716ec1eef2 | ||
|
|
b95af2d86b | ||
|
|
bca7502611 | ||
|
|
1e4fbebf49 | ||
|
|
c9e9575a3d | ||
|
|
f897a728f1 | ||
|
|
0bbb168862 | ||
|
|
0a884c7253 | ||
|
|
58e60fdfac | ||
|
|
33bd35bff4 | ||
|
|
f4b95ff5f1 | ||
|
|
f800248c10 | ||
|
|
d0b2d1dc92 | ||
|
|
85e9919bbd | ||
|
|
51fb1ab8b6 | ||
|
|
066e840e06 | ||
|
|
7031167895 | ||
|
|
69bf79d3bd | ||
|
|
909d950b50 | ||
|
|
51da1bc25a | ||
|
|
f22b623968 | ||
|
|
2bcdc03661 | ||
|
|
10d1affd81 | ||
|
|
91e7b75a44 | ||
|
|
42aaa888a1 | ||
|
|
7b8ebb0803 | ||
|
|
c270ea4e0c | ||
|
|
c93e45c0f2 | ||
|
|
19b773df85 | ||
|
|
9e7c7ec97e | ||
|
|
f735331699 | ||
|
|
5a20ef3f3f | ||
|
|
5ef054f2e0 | ||
|
|
b9fc198a7e | ||
|
|
ad4fae7f59 | ||
|
|
265de91fba | ||
|
|
7322fe40da | ||
|
|
8eb906fad9 | ||
|
|
4d9843172b | ||
|
|
e8a534be9c | ||
|
|
3148719864 | ||
|
|
abfb7afcb7 | ||
|
|
fe4ff4f835 | ||
|
|
cefc8822b6 | ||
|
|
3dc8676b99 | ||
|
|
0f112bb9c4 | ||
|
|
54e5107c34 | ||
|
|
657a068087 | ||
|
|
af6c2b5c8a | ||
|
|
d5a8fa9c5c | ||
|
|
cefde21140 | ||
|
|
160163b0cc | ||
|
|
6ce594539f | ||
|
|
4ca39ec7c3 | ||
|
|
cfdd7fbbce | ||
|
|
1cb36f4c18 | ||
|
|
602c1c64b3 | ||
|
|
3b8d6eb851 | ||
|
|
9bb98eb514 | ||
|
|
39c431c55c | ||
|
|
0171b527d8 | ||
|
|
7cccdf2205 | ||
|
|
97f3bb3da5 | ||
|
|
fc62a6cd89 | ||
|
|
23b90f5984 | ||
|
|
63ac14a19b | ||
|
|
94fd9d1657 | ||
|
|
2f89317fed | ||
|
|
38c7eaf70a | ||
|
|
02e33c3551 | ||
|
|
d5187a6a40 | ||
|
|
703032ab27 | ||
|
|
438aa3486d | ||
|
|
f2944f4d8e | ||
|
|
b8044f60fc | ||
|
|
c67b497f30 | ||
|
|
aefd9c9b41 | ||
|
|
e5d19baf3e | ||
|
|
121239bcf7 | ||
|
|
eff35e93bd | ||
|
|
0cf7952964 | ||
|
|
56f4039ac2 | ||
|
|
6641cb3799 | ||
|
|
c671ff3cf1 | ||
|
|
646ddf9c2d | ||
|
|
95abd69cc6 | ||
|
|
ab0ea753e9 | ||
|
|
9b915e996b | ||
|
|
dc948e3b6c | ||
|
|
8b6205be25 | ||
|
|
7f99cd2d2b | ||
|
|
b4fe6f3843 | ||
|
|
c29879274a | ||
|
|
512449a76d | ||
|
|
fc91047d8d | ||
|
|
6af290eb74 | ||
|
|
dd3d6f116e | ||
|
|
b4af9a31cb | ||
|
|
2862f76fca | ||
|
|
3806e5b65c | ||
|
|
673a2e35ad | ||
|
|
773c25041a | ||
|
|
e494f66c02 | ||
|
|
2833e97625 | ||
|
|
442fb88011 | ||
|
|
6b242fd277 | ||
|
|
06ed452d8f | ||
|
|
a7de947f00 | ||
|
|
dfa3fddd35 | ||
|
|
ce115cbfe1 | ||
|
|
e1d5d312b8 | ||
|
|
27565df86f | ||
|
|
7eaa60b17c | ||
|
|
7bb9936e81 | ||
|
|
ccbc5ed65b | ||
|
|
e98ec38ad8 | ||
|
|
a11e274434 | ||
|
|
f8267b13d7 | ||
|
|
3798e99ac8 | ||
|
|
fa71c40ff5 | ||
|
|
8e6edf5e34 | ||
|
|
c1e32aa9b7 | ||
|
|
b48ebeaa8a | ||
|
|
0c08b4fc8b | ||
|
|
9ae3129f16 | ||
|
|
d38c880c45 | ||
|
|
7ec2e0c524 | ||
|
|
570315687f | ||
|
|
dfd42863bb | ||
|
|
105618734c | ||
|
|
9d0701198f | ||
|
|
f9d5cb957f | ||
|
|
f7d9334445 | ||
|
|
60be2cb168 | ||
|
|
ddf8e0de4b | ||
|
|
7cc6e28916 | ||
|
|
802fcab1c6 | ||
|
|
3534396028 | ||
|
|
458aa3cc22 | ||
|
|
39b64b0af3 | ||
|
|
c66d411826 | ||
|
|
0b383b7493 | ||
|
|
1a3384e8b4 | ||
|
|
2c357265b0 | ||
|
|
c395c77cd3 | ||
|
|
57eceeea38 | ||
|
|
f75ba9172c | ||
|
|
a15d722f0e | ||
|
|
a07531d0e7 | ||
|
|
96d6cacae4 | ||
|
|
766ddfaacc | ||
|
|
5ea6cb3846 | ||
|
|
912c4804cb | ||
|
|
8f13520a1c | ||
|
|
d2d5b29e2b | ||
|
|
94a2642ce9 | ||
|
|
d0060a2b21 | ||
|
|
9b744e2fef | ||
|
|
d66dee5411 | ||
|
|
da97756157 | ||
|
|
a7b2f800f8 | ||
|
|
05831493e2 | ||
|
|
8e685b1626 | ||
|
|
87ecf552dc | ||
|
|
a6e6b6db5a | ||
|
|
d684360ebd | ||
|
|
01a133a2b8 | ||
|
|
b249ae408f | ||
|
|
04b3227b9b | ||
|
|
0a8d117129 | ||
|
|
83f26f7393 | ||
|
|
9ed6f226c6 | ||
|
|
2ba9cb1510 | ||
|
|
2e155831e6 | ||
|
|
756b858840 | ||
|
|
b2520394f4 | ||
|
|
2a97b128c3 | ||
|
|
d06da8c2da | ||
|
|
2bfb09cb11 | ||
|
|
bf733fdec5 | ||
|
|
3b4eb7c749 | ||
|
|
1119716c32 | ||
|
|
0ab23ccb51 | ||
|
|
436fcb7e85 | ||
|
|
4d2f0f2de6 | ||
|
|
82c1751f85 | ||
|
|
a2785a86dc | ||
|
|
741e89383b | ||
|
|
f64533e9e0 | ||
|
|
b13dd4e6ca | ||
|
|
35f310748e | ||
|
|
69d2cd0ac0 | ||
|
|
10c573bbc3 | ||
|
|
93030ad48d | ||
|
|
a7290f92cf | ||
|
|
b47706f360 | ||
|
|
25968925e7 | ||
|
|
fcba1183f8 | ||
|
|
75946065f2 | ||
|
|
3734c4e91d | ||
|
|
7d421bf223 | ||
|
|
41e53297c2 | ||
|
|
d4e7667ea0 | ||
|
|
daa4ddabfe | ||
|
|
8cead00bc7 | ||
|
|
db3090078b | ||
|
|
66e2fd997b | ||
|
|
a75646d047 | ||
|
|
25d1480f2a | ||
|
|
2175754a1f | ||
|
|
df5f253146 | ||
|
|
a017d9415b | ||
|
|
e89c3b1e92 | ||
|
|
d4ffeedc87 | ||
|
|
cb74b2663f | ||
|
|
4ec711bd63 | ||
|
|
e81c8ce44d | ||
|
|
c2cf348255 | ||
|
|
e048a3da38 | ||
|
|
7cf3116f5b | ||
|
|
5cd7ea06ad | ||
|
|
52c62b31fd | ||
|
|
b2bb0aeb64 | ||
|
|
f0fc87e2b6 | ||
|
|
e7a88e99f9 | ||
|
|
c3e3a36b4c | ||
|
|
7aa6c8b941 | ||
|
|
54d8d71de5 | ||
|
|
fb4c77d43b | ||
|
|
cada2f84a9 | ||
|
|
dc4627f413 | ||
|
|
02524b8b9b | ||
|
|
60b8230ecc | ||
|
|
75e6f23a82 | ||
|
|
1f221712a2 | ||
|
|
43797c03cc | ||
|
|
89637a618e | ||
|
|
fd605e0abe | ||
|
|
e73bcc73b5 | ||
|
|
c02707a90f | ||
|
|
232f853d68 | ||
|
|
91e296a0c8 | ||
|
|
bcedb06862 | ||
|
|
2ab32220ed | ||
|
|
273ccb3929 | ||
|
|
caaa4d5f35 | ||
|
|
0cf1fd1d41 | ||
|
|
5ee39df330 | ||
|
|
cc972d20f6 | ||
|
|
e0f32cfd54 | ||
|
|
6384c800c3 | ||
|
|
82de2ed8e1 | ||
|
|
af72d1854f | ||
|
|
0cff7cbccd | ||
|
|
6f4e16eed1 | ||
|
|
66be2f9240 | ||
|
|
b6c8718ae4 | ||
|
|
c8b70cc0fb | ||
|
|
6d1f621e55 | ||
|
|
671a33b31c | ||
|
|
7afc469306 | ||
|
|
8fd52248b7 | ||
|
|
69ba2aab11 | ||
|
|
f1df6dcda5 | ||
|
|
43e16bb913 | ||
|
|
4147211f94 | ||
|
|
63e49c5d3c | ||
|
|
35580c0849 | ||
|
|
8949a595fe | ||
|
|
bf8ef0a767 | ||
|
|
39962a3f48 | ||
|
|
4964621014 | ||
|
|
18e1a26da1 | ||
|
|
1d91ca5716 | ||
|
|
1040646610 | ||
|
|
fcd71931e7 | ||
|
|
bdbb74aff1 | ||
|
|
6f4029983a | ||
|
|
b2d25b1883 | ||
|
|
ba19d4f043 | ||
|
|
b222fe5afa | ||
|
|
f945defa2b | ||
|
|
4f0e4bc1ca | ||
|
|
41abc8404d | ||
|
|
2b08c4c344 | ||
|
|
97d91ddddb | ||
|
|
ec30b12fd1 | ||
|
|
9997fc11b1 | ||
|
|
c6ff0e6492 | ||
|
|
a3220ecae6 | ||
|
|
218864d08c | ||
|
|
3d0d70ece6 | ||
|
|
f629731930 | ||
|
|
e7a7b2417b | ||
|
|
0b24a9abc3 | ||
|
|
ca77b5210f | ||
|
|
0874f1c350 | ||
|
|
d89b99f42b | ||
|
|
7bd6ec68a8 | ||
|
|
bfe2eeb833 | ||
|
|
e97ab1fe3c | ||
|
|
b3ee2a8885 | ||
|
|
80b09e3212 | ||
|
|
0eb3714abc | ||
|
|
7991977443 | ||
|
|
5e5431c9f9 | ||
|
|
1fc05d1a30 | ||
|
|
21833e7c31 | ||
|
|
79daeb23a9 | ||
|
|
761c2578fb | ||
|
|
4d3145e559 | ||
|
|
91e29a3bf1 | ||
|
|
f6a4486c65 | ||
|
|
fc8b512931 | ||
|
|
e5dd15da82 | ||
|
|
e4140d71ab | ||
|
|
8312780c47 | ||
|
|
5accc3dec2 | ||
|
|
d875989866 | ||
|
|
38c92a2338 | ||
|
|
ce76b5db16 | ||
|
|
dfc4889d45 | ||
|
|
41431282ee | ||
|
|
5821b2f03c | ||
|
|
78d2bf736c | ||
|
|
6c098c3e0a | ||
|
|
bfb140d2e9 | ||
|
|
f71a1a7a89 | ||
|
|
e8aab39620 | ||
|
|
1d578d8563 | ||
|
|
abfd443541 | ||
|
|
81cbb6e5cf | ||
|
|
010c5cab87 | ||
|
|
415858119a | ||
|
|
1838a731d6 | ||
|
|
1e304fad65 | ||
|
|
999c9b3dc5 | ||
|
|
e15edbd54b | ||
|
|
e5cb77d168 | ||
|
|
cf521d4c7c | ||
|
|
6f09474193 | ||
|
|
7626933352 | ||
|
|
9e1d8c2fc6 | ||
|
|
6defed2915 | ||
|
|
d729eed7c2 | ||
|
|
f280032dcf | ||
|
|
7e85137012 | ||
|
|
88f2c3abd3 | ||
|
|
1a21e01f85 | ||
|
|
d302e817c8 | ||
|
|
1e1b0424d7 | ||
|
|
03f028b7e2 | ||
|
|
b1d35de8e4 | ||
|
|
ea6b9e5260 | ||
|
|
06d869aaa5 | ||
|
|
907cebdd6d | ||
|
|
745902bc7e | ||
|
|
ef0b3c9f9c | ||
|
|
532c077ddf | ||
|
|
cd905a6593 | ||
|
|
d0bf9d9bfb | ||
|
|
ddc79a631d | ||
|
|
6015f60db4 | ||
|
|
a6608bd7ea | ||
|
|
fb2d8c6406 | ||
|
|
c84ffb54d2 | ||
|
|
306bbdc697 | ||
|
|
9879ecad85 | ||
|
|
f0fcef5744 | ||
|
|
aa8a6058b5 | ||
|
|
48103bd244 | ||
|
|
600ac17a5f | ||
|
|
d46f28792c | ||
|
|
0f7379c941 | ||
|
|
4317fad798 | ||
|
|
5cfccb7e1d | ||
|
|
097eecd78a | ||
|
|
64b4642c49 | ||
|
|
0e87d14ca8 | ||
|
|
4d22b35a9f | ||
|
|
26586b4514 | ||
|
|
95fb2a7d7f | ||
|
|
fa66ea31d3 | ||
|
|
e0d3b819e5 | ||
|
|
17a0b4f3d0 | ||
|
|
d0d228d9f4 | ||
|
|
309acb961b | ||
|
|
12f8ebb3ea | ||
|
|
612861061c | ||
|
|
83af5ec36b | ||
|
|
74102d0319 | ||
|
|
fbd05a0fcf | ||
|
|
a53c786fe0 | ||
|
|
eb2728e5b9 | ||
|
|
3f17223387 | ||
|
|
74104cf107 | ||
|
|
13b4879723 | ||
|
|
f1ec0b2c59 | ||
|
|
6d44daf599 | ||
|
|
644a6f5569 | ||
|
|
fb83396522 | ||
|
|
e825bd0bdb | ||
|
|
61823ec7e2 | ||
|
|
cd133cbbe3 | ||
|
|
0e7a1bb76c | ||
|
|
f86bf69ebc | ||
|
|
adddf330fd | ||
|
|
10adb57b83 | ||
|
|
3160fe9abc | ||
|
|
6adb27d173 | ||
|
|
6e6aae2ea3 | ||
|
|
41a140d16c | ||
|
|
8880ab6498 | ||
|
|
389becc4f6 | ||
|
|
923530972a | ||
|
|
b84850df9f | ||
|
|
9e7dc1d11d | ||
|
|
2830ed6147 | ||
|
|
bfa919d078 | ||
|
|
f09c28e61f | ||
|
|
bfdba7713e | ||
|
|
d6cadc1e3f | ||
|
|
20a6a3f195 | ||
|
|
f60de45b52 | ||
|
|
77031d1ae4 | ||
|
|
9483a88ee1 | ||
|
|
3438a4f063 |
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -324,7 +324,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.8.2
|
||||
uses: sigstore/cosign-installer@v3.9.1
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ homeassistant.components.alert.*
|
||||
homeassistant.components.alexa.*
|
||||
homeassistant.components.alexa_devices.*
|
||||
homeassistant.components.alpha_vantage.*
|
||||
homeassistant.components.altruist.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.amberelectric.*
|
||||
homeassistant.components.ambient_network.*
|
||||
@@ -502,6 +503,7 @@ homeassistant.components.tautulli.*
|
||||
homeassistant.components.tcp.*
|
||||
homeassistant.components.technove.*
|
||||
homeassistant.components.tedee.*
|
||||
homeassistant.components.telegram_bot.*
|
||||
homeassistant.components.text.*
|
||||
homeassistant.components.thethingsnetwork.*
|
||||
homeassistant.components.threshold.*
|
||||
|
||||
16
CODEOWNERS
generated
16
CODEOWNERS
generated
@@ -93,6 +93,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/homeassistant/components/alexa_devices/ @chemelli74
|
||||
/tests/components/alexa_devices/ @chemelli74
|
||||
/homeassistant/components/altruist/ @airalab @LoSk-p
|
||||
/tests/components/altruist/ @airalab @LoSk-p
|
||||
/homeassistant/components/amazon_polly/ @jschlyter
|
||||
/homeassistant/components/amberelectric/ @madpilot
|
||||
/tests/components/amberelectric/ @madpilot
|
||||
@@ -329,8 +331,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/demo/ @home-assistant/core
|
||||
/homeassistant/components/denonavr/ @ol-iver @starkillerOG
|
||||
/tests/components/denonavr/ @ol-iver @starkillerOG
|
||||
/homeassistant/components/derivative/ @afaucogney
|
||||
/tests/components/derivative/ @afaucogney
|
||||
/homeassistant/components/derivative/ @afaucogney @karwosts
|
||||
/tests/components/derivative/ @afaucogney @karwosts
|
||||
/homeassistant/components/devialet/ @fwestenberg
|
||||
/tests/components/devialet/ @fwestenberg
|
||||
/homeassistant/components/device_automation/ @home-assistant/core
|
||||
@@ -786,8 +788,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/jellyfin/ @RunC0deRun @ctalkington
|
||||
/homeassistant/components/jewish_calendar/ @tsvi
|
||||
/tests/components/jewish_calendar/ @tsvi
|
||||
/homeassistant/components/juicenet/ @jesserockz
|
||||
/tests/components/juicenet/ @jesserockz
|
||||
/homeassistant/components/justnimbus/ @kvanzuijlen
|
||||
/tests/components/justnimbus/ @kvanzuijlen
|
||||
/homeassistant/components/jvc_projector/ @SteveEasley @msavazzi
|
||||
@@ -1169,6 +1169,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ping/ @jpbede
|
||||
/homeassistant/components/plaato/ @JohNan
|
||||
/tests/components/plaato/ @JohNan
|
||||
/homeassistant/components/playstation_network/ @jackjpowell @tr4nt0r
|
||||
/tests/components/playstation_network/ @jackjpowell @tr4nt0r
|
||||
/homeassistant/components/plex/ @jjlawren
|
||||
/tests/components/plex/ @jjlawren
|
||||
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
|
||||
@@ -1551,6 +1553,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/technove/ @Moustachauve
|
||||
/homeassistant/components/tedee/ @patrickhilker @zweckj
|
||||
/tests/components/tedee/ @patrickhilker @zweckj
|
||||
/homeassistant/components/telegram_bot/ @hanwg
|
||||
/tests/components/telegram_bot/ @hanwg
|
||||
/homeassistant/components/tellduslive/ @fredrike
|
||||
/tests/components/tellduslive/ @fredrike
|
||||
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
||||
@@ -1580,6 +1584,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tile/ @bachya
|
||||
/homeassistant/components/tilt_ble/ @apt-itude
|
||||
/tests/components/tilt_ble/ @apt-itude
|
||||
/homeassistant/components/tilt_pi/ @michaelheyman
|
||||
/tests/components/tilt_pi/ @michaelheyman
|
||||
/homeassistant/components/time/ @home-assistant/core
|
||||
/tests/components/time/ @home-assistant/core
|
||||
/homeassistant/components/time_date/ @fabaff
|
||||
@@ -1668,6 +1674,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/vallox/ @andre-richter @slovdahl @viiru- @yozik04
|
||||
/homeassistant/components/valve/ @home-assistant/core
|
||||
/tests/components/valve/ @home-assistant/core
|
||||
/homeassistant/components/vegehub/ @ghowevege
|
||||
/tests/components/vegehub/ @ghowevege
|
||||
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
||||
/tests/components/velbus/ @Cereal2nd @brefra
|
||||
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio
|
||||
|
||||
@@ -89,6 +89,7 @@ from .helpers import (
|
||||
restore_state,
|
||||
template,
|
||||
translation,
|
||||
trigger,
|
||||
)
|
||||
from .helpers.dispatcher import async_dispatcher_send_internal
|
||||
from .helpers.storage import get_internal_store_manager
|
||||
@@ -452,6 +453,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||
create_eager_task(restore_state.async_load(hass)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"domain": "sony",
|
||||
"name": "Sony",
|
||||
"integrations": ["braviatv", "ps4", "sony_projector", "songpal"]
|
||||
"integrations": [
|
||||
"braviatv",
|
||||
"ps4",
|
||||
"sony_projector",
|
||||
"songpal",
|
||||
"playstation_network"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "switchbot",
|
||||
"name": "SwitchBot",
|
||||
"integrations": ["switchbot", "switchbot_cloud"]
|
||||
"integrations": ["switchbot", "switchbot_cloud"],
|
||||
"iot_standards": ["matter"]
|
||||
}
|
||||
|
||||
5
homeassistant/brands/tilt.json
Normal file
5
homeassistant/brands/tilt.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "tilt",
|
||||
"name": "Tilt",
|
||||
"integrations": ["tilt_ble", "tilt_pi"]
|
||||
}
|
||||
@@ -185,6 +185,7 @@ FORECAST_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||
keys=[AOD_TOWN, AOD_FORECAST_DAILY, AOD_FORECAST_CURRENT, AOD_WIND_DIRECTION],
|
||||
name="Daily forecast wind bearing",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
AemetSensorEntityDescription(
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -192,6 +193,7 @@ FORECAST_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||
keys=[AOD_TOWN, AOD_FORECAST_HOURLY, AOD_FORECAST_CURRENT, AOD_WIND_DIRECTION],
|
||||
name="Hourly forecast wind bearing",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
AemetSensorEntityDescription(
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -334,7 +336,8 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||
keys=[AOD_WEATHER, AOD_WIND_DIRECTION],
|
||||
name="Wind bearing",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
),
|
||||
AemetSensorEntityDescription(
|
||||
key=ATTR_API_WIND_MAX_SPEED,
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import (
|
||||
HassJobType,
|
||||
HomeAssistant,
|
||||
@@ -17,18 +18,26 @@ from homeassistant.helpers import config_validation as cv, storage
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
||||
|
||||
from .const import DATA_COMPONENT, DATA_PREFERENCES, DOMAIN, AITaskEntityFeature
|
||||
from .const import (
|
||||
ATTR_INSTRUCTIONS,
|
||||
ATTR_TASK_NAME,
|
||||
DATA_COMPONENT,
|
||||
DATA_PREFERENCES,
|
||||
DOMAIN,
|
||||
SERVICE_GENERATE_DATA,
|
||||
AITaskEntityFeature,
|
||||
)
|
||||
from .entity import AITaskEntity
|
||||
from .http import async_setup as async_setup_conversation_http
|
||||
from .task import GenTextTask, GenTextTaskResult, async_generate_text
|
||||
from .http import async_setup as async_setup_http
|
||||
from .task import GenDataTask, GenDataTaskResult, async_generate_data
|
||||
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
"AITaskEntity",
|
||||
"AITaskEntityFeature",
|
||||
"GenTextTask",
|
||||
"GenTextTaskResult",
|
||||
"async_generate_text",
|
||||
"GenDataTask",
|
||||
"GenDataTaskResult",
|
||||
"async_generate_data",
|
||||
"async_setup",
|
||||
"async_setup_entry",
|
||||
"async_unload_entry",
|
||||
@@ -45,16 +54,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass.data[DATA_COMPONENT] = entity_component
|
||||
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
|
||||
await hass.data[DATA_PREFERENCES].async_load()
|
||||
async_setup_conversation_http(hass)
|
||||
async_setup_http(hass)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"generate_text",
|
||||
async_service_generate_text,
|
||||
SERVICE_GENERATE_DATA,
|
||||
async_service_generate_data,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required("task_name"): cv.string,
|
||||
vol.Optional("entity_id"): cv.entity_id,
|
||||
vol.Required("instructions"): cv.string,
|
||||
vol.Required(ATTR_TASK_NAME): cv.string,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
||||
}
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
@@ -73,18 +82,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
||||
|
||||
|
||||
async def async_service_generate_text(call: ServiceCall) -> ServiceResponse:
|
||||
async def async_service_generate_data(call: ServiceCall) -> ServiceResponse:
|
||||
"""Run the run task service."""
|
||||
result = await async_generate_text(hass=call.hass, **call.data)
|
||||
return result.as_dict() # type: ignore[return-value]
|
||||
result = await async_generate_data(hass=call.hass, **call.data)
|
||||
return result.as_dict()
|
||||
|
||||
|
||||
class AITaskPreferences:
|
||||
"""AI Task preferences."""
|
||||
|
||||
KEYS = ("gen_text_entity_id",)
|
||||
KEYS = ("gen_data_entity_id",)
|
||||
|
||||
gen_text_entity_id: str | None = None
|
||||
gen_data_entity_id: str | None = None
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the preferences."""
|
||||
@@ -104,11 +113,11 @@ class AITaskPreferences:
|
||||
def async_set_preferences(
|
||||
self,
|
||||
*,
|
||||
gen_text_entity_id: str | None | UndefinedType = UNDEFINED,
|
||||
gen_data_entity_id: str | None | UndefinedType = UNDEFINED,
|
||||
) -> None:
|
||||
"""Set the preferences."""
|
||||
changed = False
|
||||
for key, value in (("gen_text_entity_id", gen_text_entity_id),):
|
||||
for key, value in (("gen_data_entity_id", gen_data_entity_id),):
|
||||
if value is not UNDEFINED:
|
||||
if getattr(self, key) != value:
|
||||
setattr(self, key, value)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import IntFlag
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -17,6 +17,11 @@ DOMAIN = "ai_task"
|
||||
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
|
||||
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
|
||||
|
||||
SERVICE_GENERATE_DATA = "generate_data"
|
||||
|
||||
ATTR_INSTRUCTIONS: Final = "instructions"
|
||||
ATTR_TASK_NAME: Final = "task_name"
|
||||
|
||||
DEFAULT_SYSTEM_PROMPT = (
|
||||
"You are a Home Assistant expert and help users with their tasks."
|
||||
)
|
||||
@@ -25,5 +30,5 @@ DEFAULT_SYSTEM_PROMPT = (
|
||||
class AITaskEntityFeature(IntFlag):
|
||||
"""Supported features of the AI task entity."""
|
||||
|
||||
GENERATE_TEXT = 1
|
||||
"""Generate text based on instructions."""
|
||||
GENERATE_DATA = 1
|
||||
"""Generate data based on instructions."""
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DEFAULT_SYSTEM_PROMPT, DOMAIN, AITaskEntityFeature
|
||||
from .task import GenTextTask, GenTextTaskResult
|
||||
from .task import GenDataTask, GenDataTaskResult
|
||||
|
||||
|
||||
class AITaskEntity(RestoreEntity):
|
||||
@@ -56,7 +56,7 @@ class AITaskEntity(RestoreEntity):
|
||||
@contextlib.asynccontextmanager
|
||||
async def _async_get_ai_task_chat_log(
|
||||
self,
|
||||
task: GenTextTask,
|
||||
task: GenDataTask,
|
||||
) -> AsyncGenerator[ChatLog]:
|
||||
"""Context manager used to manage the ChatLog used during an AI Task."""
|
||||
# pylint: disable-next=contextmanager-generator-missing-cleanup
|
||||
@@ -84,20 +84,20 @@ class AITaskEntity(RestoreEntity):
|
||||
yield chat_log
|
||||
|
||||
@final
|
||||
async def internal_async_generate_text(
|
||||
async def internal_async_generate_data(
|
||||
self,
|
||||
task: GenTextTask,
|
||||
) -> GenTextTaskResult:
|
||||
"""Run a gen text task."""
|
||||
task: GenDataTask,
|
||||
) -> GenDataTaskResult:
|
||||
"""Run a gen data task."""
|
||||
self.__last_activity = dt_util.utcnow().isoformat()
|
||||
self.async_write_ha_state()
|
||||
async with self._async_get_ai_task_chat_log(task) as chat_log:
|
||||
return await self._async_generate_text(task, chat_log)
|
||||
return await self._async_generate_data(task, chat_log)
|
||||
|
||||
async def _async_generate_text(
|
||||
async def _async_generate_data(
|
||||
self,
|
||||
task: GenTextTask,
|
||||
task: GenDataTask,
|
||||
chat_log: ChatLog,
|
||||
) -> GenTextTaskResult:
|
||||
"""Handle a gen text task."""
|
||||
) -> GenDataTaskResult:
|
||||
"""Handle a gen data task."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -36,7 +36,7 @@ def websocket_get_preferences(
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "ai_task/preferences/set",
|
||||
vol.Optional("gen_text_entity_id"): vol.Any(str, None),
|
||||
vol.Optional("gen_data_entity_id"): vol.Any(str, None),
|
||||
}
|
||||
)
|
||||
@websocket_api.require_admin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"services": {
|
||||
"generate_text": {
|
||||
"generate_data": {
|
||||
"service": "mdi:file-star-four-points-outline"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
generate_text:
|
||||
generate_data:
|
||||
fields:
|
||||
task_name:
|
||||
example: "home summary"
|
||||
@@ -6,7 +6,7 @@ generate_text:
|
||||
selector:
|
||||
text:
|
||||
instructions:
|
||||
example: "Generate a funny notification that garage door was left open"
|
||||
example: "Generate a funny notification that the garage door was left open"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
@@ -16,4 +16,4 @@ generate_text:
|
||||
entity:
|
||||
domain: ai_task
|
||||
supported_features:
|
||||
- ai_task.AITaskEntityFeature.GENERATE_TEXT
|
||||
- ai_task.AITaskEntityFeature.GENERATE_DATA
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"services": {
|
||||
"generate_text": {
|
||||
"name": "Generate text",
|
||||
"description": "Use AI to run a task that generates text.",
|
||||
"generate_data": {
|
||||
"name": "Generate data",
|
||||
"description": "Uses AI to run a task that generates data.",
|
||||
"fields": {
|
||||
"task_name": {
|
||||
"name": "Task Name",
|
||||
"name": "Task name",
|
||||
"description": "Name of the task."
|
||||
},
|
||||
"instructions": {
|
||||
|
||||
@@ -3,35 +3,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
|
||||
|
||||
|
||||
async def async_generate_text(
|
||||
async def async_generate_data(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
task_name: str,
|
||||
entity_id: str | None = None,
|
||||
instructions: str,
|
||||
) -> GenTextTaskResult:
|
||||
) -> GenDataTaskResult:
|
||||
"""Run a task in the AI Task integration."""
|
||||
if entity_id is None:
|
||||
entity_id = hass.data[DATA_PREFERENCES].gen_text_entity_id
|
||||
entity_id = hass.data[DATA_PREFERENCES].gen_data_entity_id
|
||||
|
||||
if entity_id is None:
|
||||
raise ValueError("No entity_id provided and no preferred entity set")
|
||||
raise HomeAssistantError("No entity_id provided and no preferred entity set")
|
||||
|
||||
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
|
||||
if entity is None:
|
||||
raise ValueError(f"AI Task entity {entity_id} not found")
|
||||
raise HomeAssistantError(f"AI Task entity {entity_id} not found")
|
||||
|
||||
if AITaskEntityFeature.GENERATE_TEXT not in entity.supported_features:
|
||||
raise ValueError(f"AI Task entity {entity_id} does not support generating text")
|
||||
if AITaskEntityFeature.GENERATE_DATA not in entity.supported_features:
|
||||
raise HomeAssistantError(
|
||||
f"AI Task entity {entity_id} does not support generating data"
|
||||
)
|
||||
|
||||
return await entity.internal_async_generate_text(
|
||||
GenTextTask(
|
||||
return await entity.internal_async_generate_data(
|
||||
GenDataTask(
|
||||
name=task_name,
|
||||
instructions=instructions,
|
||||
)
|
||||
@@ -39,8 +43,8 @@ async def async_generate_text(
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GenTextTask:
|
||||
"""Gen text task to be processed."""
|
||||
class GenDataTask:
|
||||
"""Gen data task to be processed."""
|
||||
|
||||
name: str
|
||||
"""Name of the task."""
|
||||
@@ -50,22 +54,22 @@ class GenTextTask:
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return task as a string."""
|
||||
return f"<GenTextTask {self.name}: {id(self)}>"
|
||||
return f"<GenDataTask {self.name}: {id(self)}>"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class GenTextTaskResult:
|
||||
"""Result of gen text task."""
|
||||
class GenDataTaskResult:
|
||||
"""Result of gen data task."""
|
||||
|
||||
conversation_id: str
|
||||
"""Unique identifier for the conversation."""
|
||||
|
||||
text: str
|
||||
"""Generated text."""
|
||||
data: Any
|
||||
"""Data generated by the task."""
|
||||
|
||||
def as_dict(self) -> dict[str, str]:
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return result as a dict."""
|
||||
return {
|
||||
"conversation_id": self.conversation_id,
|
||||
"text": self.text,
|
||||
"data": self.data,
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
data = {}
|
||||
data: dict[str, Any] = {}
|
||||
try:
|
||||
obs = await self.airnow.observations.latLong(
|
||||
self.latitude,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airnow",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyairnow"],
|
||||
"requirements": ["pyairnow==1.2.1"]
|
||||
"requirements": ["pyairnow==1.3.1"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ from aioairq.core import AirQ, identify_warming_up_sensors
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
@@ -39,7 +39,7 @@ class AirQCoordinator(DataUpdateCoordinator):
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=UPDATE_INTERVAL),
|
||||
)
|
||||
session = async_get_clientsession(hass)
|
||||
session = async_create_clientsession(hass)
|
||||
self.airq = AirQ(
|
||||
entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD], session
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.NOTIFY,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
@@ -28,5 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await entry.runtime_data.api.close()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
coordinator = entry.runtime_data
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
await coordinator.api.close()
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
from aioamazondevices.const import SENSOR_STATE_OFF
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@@ -28,7 +29,8 @@ PARALLEL_UPDATES = 0
|
||||
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Alexa Devices binary sensor entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
is_on_fn: Callable[[AmazonDevice, str], bool]
|
||||
is_supported: Callable[[AmazonDevice, str], bool] = lambda device, key: True
|
||||
|
||||
|
||||
BINARY_SENSORS: Final = (
|
||||
@@ -36,13 +38,49 @@ BINARY_SENSORS: Final = (
|
||||
key="online",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
is_on_fn=lambda _device: _device.online,
|
||||
is_on_fn=lambda device, _: device.online,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="bluetooth",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
translation_key="bluetooth",
|
||||
is_on_fn=lambda _device: _device.bluetooth_state,
|
||||
is_on_fn=lambda device, _: device.bluetooth_state,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="babyCryDetectionState",
|
||||
translation_key="baby_cry_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="beepingApplianceDetectionState",
|
||||
translation_key="beeping_appliance_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="coughDetectionState",
|
||||
translation_key="cough_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="dogBarkDetectionState",
|
||||
translation_key="dog_bark_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="humanPresenceDetectionState",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="waterSoundsDetectionState",
|
||||
translation_key="water_sounds_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -60,6 +98,7 @@ async def async_setup_entry(
|
||||
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in BINARY_SENSORS
|
||||
for serial_num in coordinator.data
|
||||
if sensor_desc.is_supported(coordinator.data[serial_num], sensor_desc.key)
|
||||
)
|
||||
|
||||
|
||||
@@ -71,4 +110,6 @@ class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self.entity_description.is_on_fn(self.device)
|
||||
return self.entity_description.is_on_fn(
|
||||
self.device, self.entity_description.key
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from aioamazondevices.api import AmazonEchoApi
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect, WrongCountry
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -36,6 +36,8 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotAuthenticate:
|
||||
errors["base"] = "invalid_auth"
|
||||
except WrongCountry:
|
||||
errors["base"] = "wrong_country"
|
||||
else:
|
||||
await self.async_set_unique_id(data["customer_info"]["user_id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
@@ -2,9 +2,39 @@
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"bluetooth": {
|
||||
"default": "mdi:bluetooth",
|
||||
"default": "mdi:bluetooth-off",
|
||||
"state": {
|
||||
"off": "mdi:bluetooth-off"
|
||||
"on": "mdi:bluetooth"
|
||||
}
|
||||
},
|
||||
"baby_cry_detection": {
|
||||
"default": "mdi:account-voice-off",
|
||||
"state": {
|
||||
"on": "mdi:account-voice"
|
||||
}
|
||||
},
|
||||
"beeping_appliance_detection": {
|
||||
"default": "mdi:bell-off",
|
||||
"state": {
|
||||
"on": "mdi:bell-ring"
|
||||
}
|
||||
},
|
||||
"cough_detection": {
|
||||
"default": "mdi:blur-off",
|
||||
"state": {
|
||||
"on": "mdi:blur"
|
||||
}
|
||||
},
|
||||
"dog_bark_detection": {
|
||||
"default": "mdi:dog-side-off",
|
||||
"state": {
|
||||
"on": "mdi:dog-side"
|
||||
}
|
||||
},
|
||||
"water_sounds_detection": {
|
||||
"default": "mdi:water-pump-off",
|
||||
"state": {
|
||||
"on": "mdi:water-pump"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.1.14"]
|
||||
"requirements": ["aioamazondevices==3.2.2"]
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
from .utils import alexa_api_call
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -70,6 +71,7 @@ class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
|
||||
|
||||
entity_description: AmazonNotifyEntityDescription
|
||||
|
||||
@alexa_api_call
|
||||
async def async_send_message(
|
||||
self, message: str, title: str | None = None, **kwargs: Any
|
||||
) -> None:
|
||||
|
||||
@@ -26,7 +26,7 @@ rules:
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
|
||||
88
homeassistant/components/alexa_devices/sensor.py
Normal file
88
homeassistant/components/alexa_devices/sensor.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Support for sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import LIGHT_LUX, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonSensorEntityDescription(SensorEntityDescription):
|
||||
"""Amazon Devices sensor entity description."""
|
||||
|
||||
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
|
||||
|
||||
|
||||
SENSORS: Final = (
|
||||
AmazonSensorEntityDescription(
|
||||
key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement_fn=lambda device, _key: (
|
||||
UnitOfTemperature.CELSIUS
|
||||
if device.sensors[_key].scale == "CELSIUS"
|
||||
else UnitOfTemperature.FAHRENHEIT
|
||||
),
|
||||
),
|
||||
AmazonSensorEntityDescription(
|
||||
key="illuminance",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Amazon Devices sensors based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in SENSORS
|
||||
for serial_num in coordinator.data
|
||||
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
|
||||
)
|
||||
|
||||
|
||||
class AmazonSensorEntity(AmazonEntity, SensorEntity):
|
||||
"""Sensor device."""
|
||||
|
||||
entity_description: AmazonSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement of the sensor."""
|
||||
if self.entity_description.native_unit_of_measurement_fn:
|
||||
return self.entity_description.native_unit_of_measurement_fn(
|
||||
self.device, self.entity_description.key
|
||||
)
|
||||
|
||||
return super().native_unit_of_measurement
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.device.sensors[self.entity_description.key].value
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"data_country": "Country code",
|
||||
"data_code": "One-time password (OTP code)",
|
||||
"data_description_country": "The country of your Amazon account.",
|
||||
"data_description_country": "The country where your Amazon account is registered.",
|
||||
"data_description_username": "The email address of your Amazon account.",
|
||||
"data_description_password": "The password of your Amazon account.",
|
||||
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
|
||||
@@ -12,10 +11,10 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "[%key:component::alexa_devices::common::data_country%]",
|
||||
"country": "[%key:common::config_flow::data::country%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
"code": "[%key:component::alexa_devices::common::data_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"country": "[%key:component::alexa_devices::common::data_description_country%]",
|
||||
@@ -34,6 +33,7 @@
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
@@ -41,6 +41,21 @@
|
||||
"binary_sensor": {
|
||||
"bluetooth": {
|
||||
"name": "Bluetooth"
|
||||
},
|
||||
"baby_cry_detection": {
|
||||
"name": "Baby crying"
|
||||
},
|
||||
"beeping_appliance_detection": {
|
||||
"name": "Beeping appliance"
|
||||
},
|
||||
"cough_detection": {
|
||||
"name": "Coughing"
|
||||
},
|
||||
"dog_bark_detection": {
|
||||
"name": "Dog barking"
|
||||
},
|
||||
"water_sounds_detection": {
|
||||
"name": "Water sounds"
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
@@ -56,5 +71,13 @@
|
||||
"name": "Do not disturb"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "Error connecting: {error}"
|
||||
},
|
||||
"cannot_retrieve_data": {
|
||||
"message": "Error retrieving data: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
from .utils import alexa_api_call
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -60,6 +61,7 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
||||
|
||||
entity_description: AmazonSwitchEntityDescription
|
||||
|
||||
@alexa_api_call
|
||||
async def _switch_set_state(self, state: bool) -> None:
|
||||
"""Set desired switch state."""
|
||||
method = getattr(self.coordinator.api, self.entity_description.method)
|
||||
|
||||
40
homeassistant/components/alexa_devices/utils.py
Normal file
40
homeassistant/components/alexa_devices/utils.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Utils for Alexa Devices."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import AmazonEntity
|
||||
|
||||
|
||||
def alexa_api_call[_T: AmazonEntity, **_P](
|
||||
func: Callable[Concatenate[_T, _P], Awaitable[None]],
|
||||
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
|
||||
"""Catch Alexa API call exceptions."""
|
||||
|
||||
@wraps(func)
|
||||
async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
"""Wrap all command methods."""
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
except CannotConnect as err:
|
||||
self.coordinator.last_update_success = False
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except CannotRetrieveData as err:
|
||||
self.coordinator.last_update_success = False
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_retrieve_data",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
return cmd_wrapper
|
||||
27
homeassistant/components/altruist/__init__.py
Normal file
27
homeassistant/components/altruist/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""The Altruist integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AltruistConfigEntry, AltruistDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AltruistConfigEntry) -> bool:
|
||||
"""Set up Altruist from a config entry."""
|
||||
|
||||
coordinator = AltruistDataUpdateCoordinator(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: AltruistConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
107
homeassistant/components/altruist/config_flow.py
Normal file
107
homeassistant/components/altruist/config_flow.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Config flow for the Altruist integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from altruistclient import AltruistClient, AltruistDeviceModel, AltruistError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import CONF_HOST, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AltruistConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Altruist."""
|
||||
|
||||
device: AltruistDeviceModel
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
ip_address = ""
|
||||
if user_input is not None:
|
||||
ip_address = user_input[CONF_HOST]
|
||||
try:
|
||||
client = await AltruistClient.from_ip_address(
|
||||
async_get_clientsession(self.hass), ip_address
|
||||
)
|
||||
except AltruistError:
|
||||
errors["base"] = "no_device_found"
|
||||
else:
|
||||
self.device = client.device
|
||||
await self.async_set_unique_id(
|
||||
client.device_id, raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=self.device.id,
|
||||
data={
|
||||
CONF_HOST: ip_address,
|
||||
},
|
||||
)
|
||||
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
{CONF_HOST: ip_address},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=data_schema,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"ip_address": ip_address,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
_LOGGER.debug("Zeroconf discovery: %s", discovery_info)
|
||||
try:
|
||||
client = await AltruistClient.from_ip_address(
|
||||
async_get_clientsession(self.hass), str(discovery_info.ip_address)
|
||||
)
|
||||
except AltruistError:
|
||||
return self.async_abort(reason="no_device_found")
|
||||
|
||||
self.device = client.device
|
||||
_LOGGER.debug("Zeroconf device: %s", client.device)
|
||||
await self.async_set_unique_id(client.device_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
self.context.update(
|
||||
{
|
||||
"title_placeholders": {
|
||||
"name": self.device.id,
|
||||
}
|
||||
}
|
||||
)
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm discovery."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=self.device.id,
|
||||
data={
|
||||
CONF_HOST: self.device.ip_address,
|
||||
},
|
||||
)
|
||||
|
||||
self._set_confirm_only()
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
description_placeholders={
|
||||
"model": self.device.id,
|
||||
},
|
||||
)
|
||||
5
homeassistant/components/altruist/const.py
Normal file
5
homeassistant/components/altruist/const.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Constants for the Altruist integration."""
|
||||
|
||||
DOMAIN = "altruist"
|
||||
|
||||
CONF_HOST = "host"
|
||||
64
homeassistant/components/altruist/coordinator.py
Normal file
64
homeassistant/components/altruist/coordinator.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Coordinator module for Altruist integration in Home Assistant.
|
||||
|
||||
This module defines the AltruistDataUpdateCoordinator class, which manages
|
||||
data updates for Altruist sensors using the AltruistClient.
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from altruistclient import AltruistClient, AltruistError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_HOST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
type AltruistConfigEntry = ConfigEntry[AltruistDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AltruistDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||
"""Coordinates data updates for Altruist sensors."""
|
||||
|
||||
client: AltruistClient
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: AltruistConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the data update coordinator for Altruist sensors."""
|
||||
device_id = config_entry.unique_id
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"Altruist {device_id}",
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
)
|
||||
self._ip_address = config_entry.data[CONF_HOST]
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
try:
|
||||
self.client = await AltruistClient.from_ip_address(
|
||||
async_get_clientsession(self.hass), self._ip_address
|
||||
)
|
||||
await self.client.fetch_data()
|
||||
except AltruistError as e:
|
||||
raise ConfigEntryNotReady("Error in Altruist setup") from e
|
||||
|
||||
async def _async_update_data(self) -> dict[str, str]:
|
||||
try:
|
||||
fetched_data = await self.client.fetch_data()
|
||||
except AltruistError as ex:
|
||||
raise UpdateFailed(
|
||||
f"The Altruist {self.client.device_id} is unavailable: {ex}"
|
||||
) from ex
|
||||
return {item["value_type"]: item["value"] for item in fetched_data}
|
||||
15
homeassistant/components/altruist/icons.json
Normal file
15
homeassistant/components/altruist/icons.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"pm_10": {
|
||||
"default": "mdi:thought-bubble"
|
||||
},
|
||||
"pm_25": {
|
||||
"default": "mdi:thought-bubble-outline"
|
||||
},
|
||||
"radiation": {
|
||||
"default": "mdi:radioactive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
homeassistant/components/altruist/manifest.json
Normal file
12
homeassistant/components/altruist/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "altruist",
|
||||
"name": "Altruist",
|
||||
"codeowners": ["@airalab", "@LoSk-p"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/altruist",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["altruistclient==0.1.1"],
|
||||
"zeroconf": ["_altruist._tcp.local."]
|
||||
}
|
||||
83
homeassistant/components/altruist/quality_scale.yaml
Normal file
83
homeassistant/components/altruist/quality_scale.yaml
Normal file
@@ -0,0 +1,83 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Entities of this integration does not explicitly subscribe to events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide options flow.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: done
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device type integration
|
||||
entity-category: todo
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: No known use cases for repair issues or flows, yet
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device type integration
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
249
homeassistant/components/altruist/sensor.py
Normal file
249
homeassistant/components/altruist/sensor.py
Normal file
@@ -0,0 +1,249 @@
|
||||
"""Defines the Altruist sensor platform."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
UnitOfPressure,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AltruistConfigEntry
|
||||
from .coordinator import AltruistDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AltruistSensorEntityDescription(SensorEntityDescription):
|
||||
"""Class to describe a Sensor entity."""
|
||||
|
||||
native_value_fn: Callable[[str], float] = float
|
||||
state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS = [
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
key="BME280_humidity",
|
||||
translation_key="humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "BME280"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
key="BME280_pressure",
|
||||
translation_key="pressure",
|
||||
native_unit_of_measurement=UnitOfPressure.PA,
|
||||
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||
suggested_display_precision=0,
|
||||
translation_placeholders={"sensor_name": "BME280"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key="BME280_temperature",
|
||||
translation_key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "BME280"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
key="BMP_pressure",
|
||||
translation_key="pressure",
|
||||
native_unit_of_measurement=UnitOfPressure.PA,
|
||||
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||
suggested_display_precision=0,
|
||||
translation_placeholders={"sensor_name": "BMP"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key="BMP_temperature",
|
||||
translation_key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "BMP"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key="BMP280_temperature",
|
||||
translation_key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "BMP280"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
key="BMP280_pressure",
|
||||
translation_key="pressure",
|
||||
native_unit_of_measurement=UnitOfPressure.PA,
|
||||
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||
suggested_display_precision=0,
|
||||
translation_placeholders={"sensor_name": "BMP280"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
key="HTU21D_humidity",
|
||||
translation_key="humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "HTU21D"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key="HTU21D_temperature",
|
||||
translation_key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "HTU21D"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
translation_key="pm_10",
|
||||
key="SDS_P1",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
translation_key="pm_25",
|
||||
key="SDS_P2",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
key="SHT3X_humidity",
|
||||
translation_key="humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "SHT3X"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key="SHT3X_temperature",
|
||||
translation_key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "SHT3X"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
key="signal",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||
key="PCBA_noiseMax",
|
||||
translation_key="noise_max",
|
||||
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||
key="PCBA_noiseAvg",
|
||||
translation_key="noise_avg",
|
||||
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
translation_key="co2",
|
||||
key="CCS_CO2",
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "CCS"},
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||
key="CCS_TVOC",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
key="GC",
|
||||
native_unit_of_measurement="μR/h",
|
||||
translation_key="radiation",
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
AltruistSensorEntityDescription(
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
translation_key="co2",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
key="SCD4x_co2",
|
||||
suggested_display_precision=2,
|
||||
translation_placeholders={"sensor_name": "SCD4x"},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AltruistConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add sensors for passed config_entry in HA."""
|
||||
coordinator = config_entry.runtime_data
|
||||
async_add_entities(
|
||||
AltruistSensor(coordinator, sensor_description)
|
||||
for sensor_description in SENSOR_DESCRIPTIONS
|
||||
if sensor_description.key in coordinator.data
|
||||
)
|
||||
|
||||
|
||||
class AltruistSensor(CoordinatorEntity[AltruistDataUpdateCoordinator], SensorEntity):
|
||||
"""Implementation of a Altruist sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AltruistDataUpdateCoordinator,
|
||||
description: AltruistSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Altruist sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._device = coordinator.client.device
|
||||
self.entity_description: AltruistSensorEntityDescription = description
|
||||
self._attr_unique_id = f"{self._device.id}-{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, self._device.id)},
|
||||
manufacturer="Robonomics",
|
||||
model="Altruist",
|
||||
sw_version=self._device.fw_version,
|
||||
configuration_url=f"http://{self._device.ip_address}",
|
||||
serial_number=self._device.id,
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
super().available and self.entity_description.key in self.coordinator.data
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | int:
|
||||
"""Return the native value of the sensor."""
|
||||
string_value = self.coordinator.data[self.entity_description.key]
|
||||
return self.entity_description.native_value_fn(string_value)
|
||||
51
homeassistant/components/altruist/strings.json
Normal file
51
homeassistant/components/altruist/strings.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to start setup {model}?"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Altruist IP address or hostname in the local network"
|
||||
},
|
||||
"description": "Fill in Altruist IP address or hostname in your local network"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"no_device_found": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"humidity": {
|
||||
"name": "{sensor_name} humidity"
|
||||
},
|
||||
"pressure": {
|
||||
"name": "{sensor_name} pressure"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "{sensor_name} temperature"
|
||||
},
|
||||
"noise_max": {
|
||||
"name": "Maximum noise"
|
||||
},
|
||||
"noise_avg": {
|
||||
"name": "Average noise"
|
||||
},
|
||||
"co2": {
|
||||
"name": "{sensor_name} CO2"
|
||||
},
|
||||
"radiation": {
|
||||
"name": "Radiation level"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.camera import CameraEntityFeature
|
||||
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@@ -31,6 +32,7 @@ class IPWebcamCamera(MjpegCamera):
|
||||
"""Representation of a IP Webcam camera."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_supported_features = CameraEntityFeature.STREAM
|
||||
|
||||
def __init__(self, coordinator: AndroidIPCamDataUpdateCoordinator) -> None:
|
||||
"""Initialize the camera."""
|
||||
@@ -46,3 +48,17 @@ class IPWebcamCamera(MjpegCamera):
|
||||
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
|
||||
name=coordinator.config_entry.data[CONF_HOST],
|
||||
)
|
||||
self._coordinator = coordinator
|
||||
|
||||
async def stream_source(self) -> str:
|
||||
"""Get the stream source for the Android IP camera."""
|
||||
return self._coordinator.cam.get_rtsp_url(
|
||||
video_codec="h264", # most compatible & recommended
|
||||
# while "opus" is compatible with more devices,
|
||||
# HA's stream integration requires AAC or MP3,
|
||||
# and IP webcam doesn't provide MP3 audio.
|
||||
# aac is supported on select devices >= android 4.1.
|
||||
# The stream will be quiet on devices that don't support aac,
|
||||
# but it won't fail.
|
||||
audio_codec="aac",
|
||||
)
|
||||
|
||||
@@ -6,13 +6,24 @@ from functools import partial
|
||||
|
||||
import anthropic
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_CHAT_MODEL, DOMAIN, LOGGER, RECOMMENDED_CHAT_MODEL
|
||||
from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
DEFAULT_CONVERSATION_NAME,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
RECOMMENDED_CHAT_MODEL,
|
||||
)
|
||||
|
||||
PLATFORMS = (Platform.CONVERSATION,)
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
@@ -20,13 +31,24 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
type AnthropicConfigEntry = ConfigEntry[anthropic.AsyncClient]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Anthropic."""
|
||||
await async_migrate_integration(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> bool:
|
||||
"""Set up Anthropic from a config entry."""
|
||||
client = await hass.async_add_executor_job(
|
||||
partial(anthropic.AsyncAnthropic, api_key=entry.data[CONF_API_KEY])
|
||||
)
|
||||
try:
|
||||
model_id = entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
||||
# Use model from first conversation subentry for validation
|
||||
subentries = list(entry.subentries.values())
|
||||
if subentries:
|
||||
model_id = subentries[0].data.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
||||
else:
|
||||
model_id = RECOMMENDED_CHAT_MODEL
|
||||
model = await client.models.retrieve(model_id=model_id, timeout=10.0)
|
||||
LOGGER.debug("Anthropic model: %s", model.display_name)
|
||||
except anthropic.AuthenticationError as err:
|
||||
@@ -45,3 +67,105 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Anthropic."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
"""Migrate integration entry structure."""
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
if not any(entry.version == 1 for entry in entries):
|
||||
return
|
||||
|
||||
api_keys_entries: dict[str, ConfigEntry] = {}
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
for entry in entries:
|
||||
use_existing = False
|
||||
subentry = ConfigSubentry(
|
||||
data=entry.options,
|
||||
subentry_type="conversation",
|
||||
title=entry.title,
|
||||
unique_id=None,
|
||||
)
|
||||
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
||||
use_existing = True
|
||||
api_keys_entries[entry.data[CONF_API_KEY]] = entry
|
||||
|
||||
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
|
||||
|
||||
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
||||
conversation_entity = entity_registry.async_get_entity_id(
|
||||
"conversation",
|
||||
DOMAIN,
|
||||
entry.entry_id,
|
||||
)
|
||||
if conversation_entity is not None:
|
||||
entity_registry.async_update_entity(
|
||||
conversation_entity,
|
||||
config_entry_id=parent_entry.entry_id,
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
new_unique_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, entry.entry_id)}
|
||||
)
|
||||
if device is not None:
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
||||
add_config_subentry_id=subentry.subentry_id,
|
||||
add_config_entry_id=parent_entry.entry_id,
|
||||
)
|
||||
if parent_entry.entry_id != entry.entry_id:
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
)
|
||||
else:
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
remove_config_subentry_id=None,
|
||||
)
|
||||
|
||||
if not use_existing:
|
||||
await hass.config_entries.async_remove(entry.entry_id)
|
||||
else:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
title=DEFAULT_CONVERSATION_NAME,
|
||||
options={},
|
||||
version=2,
|
||||
minor_version=2,
|
||||
)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> bool:
|
||||
"""Migrate entry."""
|
||||
LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
|
||||
|
||||
if entry.version > 2:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 1:
|
||||
# Correct broken device migration in Home Assistant Core 2025.7.0b0-2025.7.0b1
|
||||
device_registry = dr.async_get(hass)
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
):
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
remove_config_subentry_id=None,
|
||||
)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
LOGGER.debug(
|
||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -5,20 +5,21 @@ from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
from functools import partial
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import anthropic
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigEntryState,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
ConfigSubentryFlow,
|
||||
SubentryFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import llm
|
||||
from homeassistant.helpers.selector import (
|
||||
NumberSelector,
|
||||
@@ -36,6 +37,7 @@ from .const import (
|
||||
CONF_RECOMMENDED,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_THINKING_BUDGET,
|
||||
DEFAULT_CONVERSATION_NAME,
|
||||
DOMAIN,
|
||||
RECOMMENDED_CHAT_MODEL,
|
||||
RECOMMENDED_MAX_TOKENS,
|
||||
@@ -72,7 +74,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Anthropic."""
|
||||
|
||||
VERSION = 1
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 2
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -81,6 +84,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(user_input)
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except anthropic.APITimeoutError:
|
||||
@@ -102,57 +106,93 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_create_entry(
|
||||
title="Claude",
|
||||
data=user_input,
|
||||
options=RECOMMENDED_OPTIONS,
|
||||
subentries=[
|
||||
{
|
||||
"subentry_type": "conversation",
|
||||
"data": RECOMMENDED_OPTIONS,
|
||||
"title": DEFAULT_CONVERSATION_NAME,
|
||||
"unique_id": None,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors or None
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlow:
|
||||
"""Create the options flow."""
|
||||
return AnthropicOptionsFlow(config_entry)
|
||||
@classmethod
|
||||
@callback
|
||||
def async_get_supported_subentry_types(
|
||||
cls, config_entry: ConfigEntry
|
||||
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||
"""Return subentries supported by this integration."""
|
||||
return {"conversation": ConversationSubentryFlowHandler}
|
||||
|
||||
|
||||
class AnthropicOptionsFlow(OptionsFlow):
|
||||
"""Anthropic config flow options handler."""
|
||||
class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Flow for managing conversation subentries."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.last_rendered_recommended = config_entry.options.get(
|
||||
CONF_RECOMMENDED, False
|
||||
)
|
||||
last_rendered_recommended = False
|
||||
|
||||
async def async_step_init(
|
||||
@property
|
||||
def _is_new(self) -> bool:
|
||||
"""Return if this is a new subentry."""
|
||||
return self.source == "user"
|
||||
|
||||
async def async_step_set_options(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
|
||||
) -> SubentryFlowResult:
|
||||
"""Set conversation options."""
|
||||
# abort if entry is not loaded
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||
if not user_input.get(CONF_LLM_HASS_API):
|
||||
user_input.pop(CONF_LLM_HASS_API, None)
|
||||
if user_input.get(
|
||||
CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET
|
||||
) >= user_input.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS):
|
||||
errors[CONF_THINKING_BUDGET] = "thinking_budget_too_large"
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
if user_input is None:
|
||||
if self._is_new:
|
||||
options = RECOMMENDED_OPTIONS.copy()
|
||||
else:
|
||||
# Re-render the options again, now with the recommended options shown/hidden
|
||||
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
|
||||
# If this is a reconfiguration, we need to copy the existing options
|
||||
# so that we can show the current values in the form.
|
||||
options = self._get_reconfigure_subentry().data.copy()
|
||||
|
||||
options = {
|
||||
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
|
||||
CONF_PROMPT: user_input[CONF_PROMPT],
|
||||
CONF_LLM_HASS_API: user_input.get(CONF_LLM_HASS_API),
|
||||
}
|
||||
self.last_rendered_recommended = cast(
|
||||
bool, options.get(CONF_RECOMMENDED, False)
|
||||
)
|
||||
|
||||
elif user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||
if not user_input.get(CONF_LLM_HASS_API):
|
||||
user_input.pop(CONF_LLM_HASS_API, None)
|
||||
if user_input.get(
|
||||
CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET
|
||||
) >= user_input.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS):
|
||||
errors[CONF_THINKING_BUDGET] = "thinking_budget_too_large"
|
||||
|
||||
if not errors:
|
||||
if self._is_new:
|
||||
return self.async_create_entry(
|
||||
title=user_input.pop(CONF_NAME),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
self._get_reconfigure_subentry(),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
options = user_input
|
||||
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
|
||||
else:
|
||||
# Re-render the options again, now with the recommended options shown/hidden
|
||||
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
|
||||
|
||||
options = {
|
||||
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
|
||||
CONF_PROMPT: user_input[CONF_PROMPT],
|
||||
CONF_LLM_HASS_API: user_input.get(CONF_LLM_HASS_API),
|
||||
}
|
||||
|
||||
suggested_values = options.copy()
|
||||
if not suggested_values.get(CONF_PROMPT):
|
||||
@@ -163,19 +203,25 @@ class AnthropicOptionsFlow(OptionsFlow):
|
||||
suggested_values[CONF_LLM_HASS_API] = [suggested_llm_apis]
|
||||
|
||||
schema = self.add_suggested_values_to_schema(
|
||||
vol.Schema(anthropic_config_option_schema(self.hass, options)),
|
||||
vol.Schema(
|
||||
anthropic_config_option_schema(self.hass, self._is_new, options)
|
||||
),
|
||||
suggested_values,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
step_id="set_options",
|
||||
data_schema=schema,
|
||||
errors=errors or None,
|
||||
)
|
||||
|
||||
async_step_user = async_step_set_options
|
||||
async_step_reconfigure = async_step_set_options
|
||||
|
||||
|
||||
def anthropic_config_option_schema(
|
||||
hass: HomeAssistant,
|
||||
is_new: bool,
|
||||
options: Mapping[str, Any],
|
||||
) -> dict:
|
||||
"""Return a schema for Anthropic completion options."""
|
||||
@@ -187,15 +233,24 @@ def anthropic_config_option_schema(
|
||||
for api in llm.async_get_apis(hass)
|
||||
]
|
||||
|
||||
schema = {
|
||||
vol.Optional(CONF_PROMPT): TemplateSelector(),
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
): SelectSelector(SelectSelectorConfig(options=hass_apis, multiple=True)),
|
||||
vol.Required(
|
||||
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
|
||||
): bool,
|
||||
}
|
||||
if is_new:
|
||||
schema: dict[vol.Required | vol.Optional, Any] = {
|
||||
vol.Required(CONF_NAME, default=DEFAULT_CONVERSATION_NAME): str,
|
||||
}
|
||||
else:
|
||||
schema = {}
|
||||
|
||||
schema.update(
|
||||
{
|
||||
vol.Optional(CONF_PROMPT): TemplateSelector(),
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
): SelectSelector(SelectSelectorConfig(options=hass_apis, multiple=True)),
|
||||
vol.Required(
|
||||
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
|
||||
if options.get(CONF_RECOMMENDED):
|
||||
return schema
|
||||
|
||||
@@ -5,6 +5,8 @@ import logging
|
||||
DOMAIN = "anthropic"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DEFAULT_CONVERSATION_NAME = "Claude conversation"
|
||||
|
||||
CONF_RECOMMENDED = "recommended"
|
||||
CONF_PROMPT = "prompt"
|
||||
CONF_CHAT_MODEL = "chat_model"
|
||||
|
||||
@@ -38,7 +38,7 @@ from anthropic.types import (
|
||||
from voluptuous_openapi import convert
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
|
||||
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -72,8 +72,14 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up conversation entities."""
|
||||
agent = AnthropicConversationEntity(config_entry)
|
||||
async_add_entities([agent])
|
||||
for subentry in config_entry.subentries.values():
|
||||
if subentry.subentry_type != "conversation":
|
||||
continue
|
||||
|
||||
async_add_entities(
|
||||
[AnthropicConversationEntity(config_entry, subentry)],
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
|
||||
def _format_tool(
|
||||
@@ -326,21 +332,22 @@ class AnthropicConversationEntity(
|
||||
):
|
||||
"""Anthropic conversation agent."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_supports_streaming = True
|
||||
|
||||
def __init__(self, entry: AnthropicConfigEntry) -> None:
|
||||
def __init__(self, entry: AnthropicConfigEntry, subentry: ConfigSubentry) -> None:
|
||||
"""Initialize the agent."""
|
||||
self.entry = entry
|
||||
self._attr_unique_id = entry.entry_id
|
||||
self.subentry = subentry
|
||||
self._attr_name = subentry.title
|
||||
self._attr_unique_id = subentry.subentry_id
|
||||
self._attr_device_info = dr.DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
identifiers={(DOMAIN, subentry.subentry_id)},
|
||||
name=subentry.title,
|
||||
manufacturer="Anthropic",
|
||||
model="Claude",
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
if self.entry.options.get(CONF_LLM_HASS_API):
|
||||
if self.subentry.data.get(CONF_LLM_HASS_API):
|
||||
self._attr_supported_features = (
|
||||
conversation.ConversationEntityFeature.CONTROL
|
||||
)
|
||||
@@ -363,7 +370,7 @@ class AnthropicConversationEntity(
|
||||
chat_log: conversation.ChatLog,
|
||||
) -> conversation.ConversationResult:
|
||||
"""Call the API."""
|
||||
options = self.entry.options
|
||||
options = self.subentry.data
|
||||
|
||||
try:
|
||||
await chat_log.async_provide_llm_data(
|
||||
@@ -393,7 +400,7 @@ class AnthropicConversationEntity(
|
||||
chat_log: conversation.ChatLog,
|
||||
) -> None:
|
||||
"""Generate an answer for the chat log."""
|
||||
options = self.entry.options
|
||||
options = self.subentry.data
|
||||
|
||||
tools: list[ToolParam] | None = None
|
||||
if chat_log.llm_api:
|
||||
|
||||
@@ -12,28 +12,44 @@
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
|
||||
"authentication_error": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"prompt": "Instructions",
|
||||
"chat_model": "[%key:common::generic::model%]",
|
||||
"max_tokens": "Maximum tokens to return in response",
|
||||
"temperature": "Temperature",
|
||||
"llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]",
|
||||
"recommended": "Recommended model settings",
|
||||
"thinking_budget_tokens": "Thinking budget"
|
||||
},
|
||||
"data_description": {
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template.",
|
||||
"thinking_budget_tokens": "The number of tokens the model can use to think about the response out of the total maximum number of tokens. Set to 1024 or greater to enable extended thinking."
|
||||
"config_subentries": {
|
||||
"conversation": {
|
||||
"initiate_flow": {
|
||||
"user": "Add conversation agent",
|
||||
"reconfigure": "Reconfigure conversation agent"
|
||||
},
|
||||
"entry_type": "Conversation agent",
|
||||
|
||||
"step": {
|
||||
"set_options": {
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"prompt": "Instructions",
|
||||
"chat_model": "[%key:common::generic::model%]",
|
||||
"max_tokens": "Maximum tokens to return in response",
|
||||
"temperature": "Temperature",
|
||||
"llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]",
|
||||
"recommended": "Recommended model settings",
|
||||
"thinking_budget_tokens": "Thinking budget"
|
||||
},
|
||||
"data_description": {
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template.",
|
||||
"thinking_budget_tokens": "The number of tokens the model can use to think about the response out of the total maximum number of tokens. Set to 1024 or greater to enable extended thinking."
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"entry_not_loaded": "Cannot add things while the configuration is disabled."
|
||||
},
|
||||
"error": {
|
||||
"thinking_budget_too_large": "Maximum tokens must be greater than the thinking budget."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"thinking_budget_too_large": "Maximum tokens must be greater than the thinking budget."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,11 +260,18 @@ class APIEntityStateView(HomeAssistantView):
|
||||
if not user.is_admin:
|
||||
raise Unauthorized(entity_id=entity_id)
|
||||
hass = request.app[KEY_HASS]
|
||||
|
||||
body = await request.text()
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
data: Any = json_loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message("Invalid JSON specified.", HTTPStatus.BAD_REQUEST)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return self.json_message(
|
||||
"State data should be a JSON object.", HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
if (new_state := data.get("state")) is None:
|
||||
return self.json_message("No state specified.", HTTPStatus.BAD_REQUEST)
|
||||
|
||||
@@ -477,9 +484,19 @@ class APITemplateView(HomeAssistantView):
|
||||
@require_admin
|
||||
async def post(self, request: web.Request) -> web.Response:
|
||||
"""Render a template."""
|
||||
body = await request.text()
|
||||
|
||||
try:
|
||||
data: Any = json_loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message("Invalid JSON specified.", HTTPStatus.BAD_REQUEST)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return self.json_message(
|
||||
"Template data should be a JSON object.", HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
tpl = _cached_template(data["template"], request.app[KEY_HASS])
|
||||
try:
|
||||
data = await request.json()
|
||||
tpl = _cached_template(data["template"], request.app[KEY_HASS])
|
||||
return tpl.async_render(variables=data.get("variables"), parse_result=False) # type: ignore[no-any-return]
|
||||
except (ValueError, TemplateError) as ex:
|
||||
return self.json_message(
|
||||
|
||||
@@ -1119,6 +1119,7 @@ class PipelineRun:
|
||||
) is not None:
|
||||
# Sentence trigger matched
|
||||
agent_id = "sentence_trigger"
|
||||
processed_locally = True
|
||||
intent_response = intent.IntentResponse(
|
||||
self.pipeline.conversation_language
|
||||
)
|
||||
|
||||
@@ -71,9 +71,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
cv.make_entity_service_schema(
|
||||
{
|
||||
vol.Optional("message"): str,
|
||||
vol.Optional("media_id"): str,
|
||||
vol.Optional("media_id"): _media_id_validator,
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): _media_id_validator,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key("message", "media_id"),
|
||||
@@ -81,15 +81,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"async_internal_announce",
|
||||
[AssistSatelliteEntityFeature.ANNOUNCE],
|
||||
)
|
||||
|
||||
component.async_register_entity_service(
|
||||
"start_conversation",
|
||||
vol.All(
|
||||
cv.make_entity_service_schema(
|
||||
{
|
||||
vol.Optional("start_message"): str,
|
||||
vol.Optional("start_media_id"): str,
|
||||
vol.Optional("start_media_id"): _media_id_validator,
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): _media_id_validator,
|
||||
vol.Optional("extra_system_prompt"): str,
|
||||
}
|
||||
),
|
||||
@@ -135,9 +136,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_domain(DOMAIN),
|
||||
vol.Optional("question"): str,
|
||||
vol.Optional("question_media_id"): str,
|
||||
vol.Optional("question_media_id"): _media_id_validator,
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): _media_id_validator,
|
||||
vol.Optional("answers"): [
|
||||
{
|
||||
vol.Required("id"): str,
|
||||
@@ -204,3 +205,20 @@ def has_one_non_empty_item(value: list[str]) -> list[str]:
|
||||
raise vol.Invalid("sentences cannot be empty")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
# Validator for media_id fields that accepts both string and media selector format
|
||||
_media_id_validator = vol.Any(
|
||||
cv.string, # Plain string format
|
||||
vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required("media_content_id"): cv.string,
|
||||
vol.Required("media_content_type"): cv.string,
|
||||
vol.Remove("metadata"): dict, # Ignore metadata if present
|
||||
}
|
||||
),
|
||||
# Extract media_content_id from media selector format
|
||||
lambda x: x["media_content_id"],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -14,7 +14,9 @@ announce:
|
||||
media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
media:
|
||||
accept:
|
||||
- audio/*
|
||||
preannounce:
|
||||
required: false
|
||||
default: true
|
||||
@@ -23,7 +25,9 @@ announce:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
media:
|
||||
accept:
|
||||
- audio/*
|
||||
start_conversation:
|
||||
target:
|
||||
entity:
|
||||
@@ -40,7 +44,9 @@ start_conversation:
|
||||
start_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
media:
|
||||
accept:
|
||||
- audio/*
|
||||
extra_system_prompt:
|
||||
required: false
|
||||
selector:
|
||||
@@ -53,7 +59,9 @@ start_conversation:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
media:
|
||||
accept:
|
||||
- audio/*
|
||||
ask_question:
|
||||
fields:
|
||||
entity_id:
|
||||
@@ -72,7 +80,9 @@ ask_question:
|
||||
question_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
media:
|
||||
accept:
|
||||
- audio/*
|
||||
preannounce:
|
||||
required: false
|
||||
default: true
|
||||
@@ -81,8 +91,24 @@ ask_question:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
media:
|
||||
accept:
|
||||
- audio/*
|
||||
answers:
|
||||
required: false
|
||||
selector:
|
||||
object:
|
||||
label_field: sentences
|
||||
description_field: id
|
||||
multiple: true
|
||||
translation_key: answers
|
||||
fields:
|
||||
id:
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
sentences:
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
multiple: true
|
||||
|
||||
@@ -90,5 +90,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"answers": {
|
||||
"fields": {
|
||||
"id": "Answer ID",
|
||||
"sentences": "Sentences"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"bluetooth-data-tools==1.28.2",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.49.0"
|
||||
]
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bosch_alarm",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["bosch-alarm-mode2==0.4.6"]
|
||||
}
|
||||
|
||||
@@ -28,38 +28,41 @@ rules:
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: todo
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
No options flow is provided.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device type integration
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
|
||||
@@ -168,7 +168,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
key="windazimuth",
|
||||
translation_key="windazimuth",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
icon="mdi:compass-outline",
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||
),
|
||||
|
||||
@@ -498,19 +498,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Flag supported features."""
|
||||
return self._attr_supported_features
|
||||
|
||||
@property
|
||||
def supported_features_compat(self) -> CameraEntityFeature:
|
||||
"""Return the supported features as CameraEntityFeature.
|
||||
|
||||
Remove this compatibility shim in 2025.1 or later.
|
||||
"""
|
||||
features = self.supported_features
|
||||
if type(features) is int:
|
||||
new_features = CameraEntityFeature(features)
|
||||
self._report_deprecated_supported_features_values(new_features)
|
||||
return new_features
|
||||
return features
|
||||
|
||||
@cached_property
|
||||
def is_recording(self) -> bool:
|
||||
"""Return true if the device is recording."""
|
||||
@@ -704,9 +691,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
async def async_internal_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_internal_added_to_hass()
|
||||
self.__supports_stream = (
|
||||
self.supported_features_compat & CameraEntityFeature.STREAM
|
||||
)
|
||||
self.__supports_stream = self.supported_features & CameraEntityFeature.STREAM
|
||||
await self.async_refresh_providers(write_state=False)
|
||||
|
||||
async def async_refresh_providers(self, *, write_state: bool = True) -> None:
|
||||
@@ -735,7 +720,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
self, fn: Callable[[HomeAssistant, Camera], Coroutine[None, None, _T | None]]
|
||||
) -> _T | None:
|
||||
"""Get first provider that supports this camera."""
|
||||
if CameraEntityFeature.STREAM not in self.supported_features_compat:
|
||||
if CameraEntityFeature.STREAM not in self.supported_features:
|
||||
return None
|
||||
|
||||
return await fn(self.hass, self)
|
||||
@@ -785,7 +770,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
def camera_capabilities(self) -> CameraCapabilities:
|
||||
"""Return the camera capabilities."""
|
||||
frontend_stream_types = set()
|
||||
if CameraEntityFeature.STREAM in self.supported_features_compat:
|
||||
if CameraEntityFeature.STREAM in self.supported_features:
|
||||
if self._supports_native_async_webrtc:
|
||||
# The camera has a native WebRTC implementation
|
||||
frontend_stream_types.add(StreamType.WEB_RTC)
|
||||
@@ -805,8 +790,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""
|
||||
super().async_write_ha_state()
|
||||
if self.__supports_stream != (
|
||||
supports_stream := self.supported_features_compat
|
||||
& CameraEntityFeature.STREAM
|
||||
supports_stream := self.supported_features & CameraEntityFeature.STREAM
|
||||
):
|
||||
self.__supports_stream = supports_stream
|
||||
self._invalidate_camera_capabilities_cache()
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.103.0"],
|
||||
"requirements": ["hass-nabucasa==0.104.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.10"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.23"]
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_MAX_SUB_INTERVAL,
|
||||
CONF_ROUND_DIGITS,
|
||||
CONF_TIME_WINDOW,
|
||||
CONF_UNIT_PREFIX,
|
||||
@@ -104,6 +105,9 @@ async def _get_options_dict(handler: SchemaCommonFlowHandler | None) -> dict:
|
||||
options=TIME_UNITS, translation_key="time_unit"
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_MAX_SUB_INTERVAL): selector.DurationSelector(
|
||||
selector.DurationSelectorConfig(allow_negative=False)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,3 +7,4 @@ CONF_TIME_WINDOW = "time_window"
|
||||
CONF_UNIT = "unit"
|
||||
CONF_UNIT_PREFIX = "unit_prefix"
|
||||
CONF_UNIT_TIME = "unit_time"
|
||||
CONF_MAX_SUB_INTERVAL = "max_sub_interval"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "derivative",
|
||||
"name": "Derivative",
|
||||
"after_dependencies": ["counter"],
|
||||
"codeowners": ["@afaucogney"],
|
||||
"codeowners": ["@afaucogney", "@karwosts"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/derivative",
|
||||
"integration_type": "helper",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal, DecimalException
|
||||
from decimal import Decimal, DecimalException, InvalidOperation
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -25,6 +25,7 @@ from homeassistant.const import (
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
EventStateChangedData,
|
||||
EventStateReportedData,
|
||||
@@ -40,12 +41,14 @@ from homeassistant.helpers.entity_platform import (
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.event import (
|
||||
async_call_later,
|
||||
async_track_state_change_event,
|
||||
async_track_state_report_event,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
CONF_MAX_SUB_INTERVAL,
|
||||
CONF_ROUND_DIGITS,
|
||||
CONF_TIME_WINDOW,
|
||||
CONF_UNIT,
|
||||
@@ -89,10 +92,20 @@ PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
vol.Optional(CONF_UNIT_TIME, default=UnitOfTime.HOURS): vol.In(UNIT_TIME),
|
||||
vol.Optional(CONF_UNIT): cv.string,
|
||||
vol.Optional(CONF_TIME_WINDOW, default=DEFAULT_TIME_WINDOW): cv.time_period,
|
||||
vol.Optional(CONF_MAX_SUB_INTERVAL): cv.positive_time_period,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _is_decimal_state(state: str) -> bool:
|
||||
try:
|
||||
Decimal(state)
|
||||
except (InvalidOperation, TypeError):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -114,6 +127,11 @@ async def async_setup_entry(
|
||||
# Before we had support for optional selectors, "none" was used for selecting nothing
|
||||
unit_prefix = None
|
||||
|
||||
if max_sub_interval_dict := config_entry.options.get(CONF_MAX_SUB_INTERVAL, None):
|
||||
max_sub_interval = cv.time_period(max_sub_interval_dict)
|
||||
else:
|
||||
max_sub_interval = None
|
||||
|
||||
derivative_sensor = DerivativeSensor(
|
||||
name=config_entry.title,
|
||||
round_digits=int(config_entry.options[CONF_ROUND_DIGITS]),
|
||||
@@ -124,6 +142,7 @@ async def async_setup_entry(
|
||||
unit_prefix=unit_prefix,
|
||||
unit_time=config_entry.options[CONF_UNIT_TIME],
|
||||
device_info=device_info,
|
||||
max_sub_interval=max_sub_interval,
|
||||
)
|
||||
|
||||
async_add_entities([derivative_sensor])
|
||||
@@ -145,6 +164,7 @@ async def async_setup_platform(
|
||||
unit_prefix=config[CONF_UNIT_PREFIX],
|
||||
unit_time=config[CONF_UNIT_TIME],
|
||||
unique_id=None,
|
||||
max_sub_interval=config.get(CONF_MAX_SUB_INTERVAL),
|
||||
)
|
||||
|
||||
async_add_entities([derivative])
|
||||
@@ -166,6 +186,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
unit_of_measurement: str | None,
|
||||
unit_prefix: str | None,
|
||||
unit_time: UnitOfTime,
|
||||
max_sub_interval: timedelta | None,
|
||||
unique_id: str | None,
|
||||
device_info: DeviceInfo | None = None,
|
||||
) -> None:
|
||||
@@ -192,6 +213,34 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
self._unit_prefix = UNIT_PREFIXES[unit_prefix]
|
||||
self._unit_time = UNIT_TIME[unit_time]
|
||||
self._time_window = time_window.total_seconds()
|
||||
self._max_sub_interval: timedelta | None = (
|
||||
None # disable time based derivative
|
||||
if max_sub_interval is None or max_sub_interval.total_seconds() == 0
|
||||
else max_sub_interval
|
||||
)
|
||||
self._cancel_max_sub_interval_exceeded_callback: CALLBACK_TYPE = (
|
||||
lambda *args: None
|
||||
)
|
||||
|
||||
def _calc_derivative_from_state_list(self, current_time: datetime) -> Decimal:
|
||||
def calculate_weight(start: datetime, end: datetime, now: datetime) -> float:
|
||||
window_start = now - timedelta(seconds=self._time_window)
|
||||
return (end - max(start, window_start)).total_seconds() / self._time_window
|
||||
|
||||
derivative = Decimal("0.00")
|
||||
for start, end, value in self._state_list:
|
||||
weight = calculate_weight(start, end, current_time)
|
||||
derivative = derivative + (value * Decimal(weight))
|
||||
|
||||
return derivative
|
||||
|
||||
def _prune_state_list(self, current_time: datetime) -> None:
|
||||
# filter out all derivatives older than `time_window` from our window list
|
||||
self._state_list = [
|
||||
(time_start, time_end, state)
|
||||
for time_start, time_end, state in self._state_list
|
||||
if (current_time - time_end).total_seconds() < self._time_window
|
||||
]
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
@@ -209,13 +258,52 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
except SyntaxError as err:
|
||||
_LOGGER.warning("Could not restore last state: %s", err)
|
||||
|
||||
def schedule_max_sub_interval_exceeded(source_state: State | None) -> None:
|
||||
"""Schedule calculation using the source state and max_sub_interval.
|
||||
|
||||
The callback reference is stored for possible cancellation if the source state
|
||||
reports a change before max_sub_interval has passed.
|
||||
If the callback is executed, meaning there was no state change reported, the
|
||||
source_state is assumed constant and calculation is done using its value.
|
||||
"""
|
||||
if (
|
||||
self._max_sub_interval is not None
|
||||
and source_state is not None
|
||||
and (_is_decimal_state(source_state.state))
|
||||
):
|
||||
|
||||
@callback
|
||||
def _calc_derivative_on_max_sub_interval_exceeded_callback(
|
||||
now: datetime,
|
||||
) -> None:
|
||||
"""Calculate derivative based on time and reschedule."""
|
||||
|
||||
self._prune_state_list(now)
|
||||
derivative = self._calc_derivative_from_state_list(now)
|
||||
self._attr_native_value = round(derivative, self._round_digits)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
# If derivative is now zero, don't schedule another timeout callback, as it will have no effect
|
||||
if derivative != 0:
|
||||
schedule_max_sub_interval_exceeded(source_state)
|
||||
|
||||
self._cancel_max_sub_interval_exceeded_callback = async_call_later(
|
||||
self.hass,
|
||||
self._max_sub_interval,
|
||||
_calc_derivative_on_max_sub_interval_exceeded_callback,
|
||||
)
|
||||
|
||||
@callback
|
||||
def on_state_reported(event: Event[EventStateReportedData]) -> None:
|
||||
"""Handle constant sensor state."""
|
||||
self._cancel_max_sub_interval_exceeded_callback()
|
||||
new_state = event.data["new_state"]
|
||||
if self._attr_native_value == Decimal(0):
|
||||
# If the derivative is zero, and the source sensor hasn't
|
||||
# changed state, then we know it will still be zero.
|
||||
return
|
||||
schedule_max_sub_interval_exceeded(new_state)
|
||||
new_state = event.data["new_state"]
|
||||
if new_state is not None:
|
||||
calc_derivative(
|
||||
@@ -225,7 +313,9 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
@callback
|
||||
def on_state_changed(event: Event[EventStateChangedData]) -> None:
|
||||
"""Handle changed sensor state."""
|
||||
self._cancel_max_sub_interval_exceeded_callback()
|
||||
new_state = event.data["new_state"]
|
||||
schedule_max_sub_interval_exceeded(new_state)
|
||||
old_state = event.data["old_state"]
|
||||
if new_state is not None and old_state is not None:
|
||||
calc_derivative(new_state, old_state.state, old_state.last_reported)
|
||||
@@ -246,13 +336,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
"" if unit is None else unit
|
||||
)
|
||||
|
||||
# filter out all derivatives older than `time_window` from our window list
|
||||
self._state_list = [
|
||||
(time_start, time_end, state)
|
||||
for time_start, time_end, state in self._state_list
|
||||
if (new_state.last_reported - time_end).total_seconds()
|
||||
< self._time_window
|
||||
]
|
||||
self._prune_state_list(new_state.last_reported)
|
||||
|
||||
try:
|
||||
elapsed_time = (
|
||||
@@ -290,28 +374,27 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
|
||||
(old_last_reported, new_state.last_reported, new_derivative)
|
||||
)
|
||||
|
||||
def calculate_weight(
|
||||
start: datetime, end: datetime, now: datetime
|
||||
) -> float:
|
||||
window_start = now - timedelta(seconds=self._time_window)
|
||||
if start < window_start:
|
||||
weight = (end - window_start).total_seconds() / self._time_window
|
||||
else:
|
||||
weight = (end - start).total_seconds() / self._time_window
|
||||
return weight
|
||||
|
||||
# If outside of time window just report derivative (is the same as modeling it in the window),
|
||||
# otherwise take the weighted average with the previous derivatives
|
||||
if elapsed_time > self._time_window:
|
||||
derivative = new_derivative
|
||||
else:
|
||||
derivative = Decimal("0.00")
|
||||
for start, end, value in self._state_list:
|
||||
weight = calculate_weight(start, end, new_state.last_reported)
|
||||
derivative = derivative + (value * Decimal(weight))
|
||||
derivative = self._calc_derivative_from_state_list(
|
||||
new_state.last_reported
|
||||
)
|
||||
self._attr_native_value = round(derivative, self._round_digits)
|
||||
self.async_write_ha_state()
|
||||
|
||||
if self._max_sub_interval is not None:
|
||||
source_state = self.hass.states.get(self._sensor_source_id)
|
||||
schedule_max_sub_interval_exceeded(source_state)
|
||||
|
||||
@callback
|
||||
def on_removed() -> None:
|
||||
self._cancel_max_sub_interval_exceeded_callback()
|
||||
|
||||
self.async_on_remove(on_removed)
|
||||
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, self._sensor_source_id, on_state_changed
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"title": "Create Derivative sensor",
|
||||
"description": "Create a sensor that estimates the derivative of a sensor.",
|
||||
"data": {
|
||||
"max_sub_interval": "Max sub-interval",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"round": "Precision",
|
||||
"source": "Input sensor",
|
||||
@@ -14,6 +15,7 @@
|
||||
"unit_time": "Time unit"
|
||||
},
|
||||
"data_description": {
|
||||
"max_sub_interval": "If defined, derivative automatically recalculates if the source has not updated for this duration.",
|
||||
"round": "Controls the number of decimal digits in the output.",
|
||||
"time_window": "If set, the sensor's value is a time-weighted moving average of derivatives within this window.",
|
||||
"unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative."
|
||||
@@ -25,6 +27,7 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"max_sub_interval": "[%key:component::derivative::config::step::user::data::max_sub_interval%]",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"round": "[%key:component::derivative::config::step::user::data::round%]",
|
||||
"source": "[%key:component::derivative::config::step::user::data::source%]",
|
||||
@@ -33,6 +36,7 @@
|
||||
"unit_time": "[%key:component::derivative::config::step::user::data::unit_time%]"
|
||||
},
|
||||
"data_description": {
|
||||
"max_sub_interval": "[%key:component::derivative::config::step::user::data_description::max_sub_interval%]",
|
||||
"round": "[%key:component::derivative::config::step::user::data_description::round%]",
|
||||
"time_window": "[%key:component::derivative::config::step::user::data_description::time_window%]",
|
||||
"unit_prefix": "[%key:component::derivative::config::step::user::data_description::unit_prefix%]"
|
||||
|
||||
@@ -9,7 +9,11 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.condition import ConditionProtocol, trace_condition_function
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
ConditionCheckerType,
|
||||
trace_condition_function,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import DeviceAutomationType, async_get_device_automation_platform
|
||||
@@ -19,13 +23,24 @@ if TYPE_CHECKING:
|
||||
from homeassistant.helpers import condition
|
||||
|
||||
|
||||
class DeviceAutomationConditionProtocol(ConditionProtocol, Protocol):
|
||||
class DeviceAutomationConditionProtocol(Protocol):
|
||||
"""Define the format of device_condition modules.
|
||||
|
||||
Each module must define either CONDITION_SCHEMA or async_validate_condition_config
|
||||
from ConditionProtocol.
|
||||
Each module must define either CONDITION_SCHEMA or async_validate_condition_config.
|
||||
"""
|
||||
|
||||
CONDITION_SCHEMA: vol.Schema
|
||||
|
||||
async def async_validate_condition_config(
|
||||
self, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
|
||||
def async_condition_from_config(
|
||||
self, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConditionCheckerType:
|
||||
"""Evaluate state based on configuration."""
|
||||
|
||||
async def async_get_condition_capabilities(
|
||||
self, hass: HomeAssistant, config: ConfigType
|
||||
) -> dict[str, vol.Schema]:
|
||||
@@ -37,20 +52,38 @@ class DeviceAutomationConditionProtocol(ConditionProtocol, Protocol):
|
||||
"""List conditions."""
|
||||
|
||||
|
||||
async def async_validate_condition_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate device condition config."""
|
||||
return await async_validate_device_automation_config(
|
||||
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
|
||||
)
|
||||
class DeviceCondition(Condition):
|
||||
"""Device condition."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Initialize condition."""
|
||||
self._config = config
|
||||
self._hass = hass
|
||||
|
||||
@classmethod
|
||||
async def async_validate_condition_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate device condition config."""
|
||||
return await async_validate_device_automation_config(
|
||||
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
|
||||
)
|
||||
|
||||
async def async_condition_from_config(self) -> condition.ConditionCheckerType:
|
||||
"""Test a device condition."""
|
||||
platform = await async_get_device_automation_platform(
|
||||
self._hass, self._config[CONF_DOMAIN], DeviceAutomationType.CONDITION
|
||||
)
|
||||
return trace_condition_function(
|
||||
platform.async_condition_from_config(self._hass, self._config)
|
||||
)
|
||||
|
||||
|
||||
async def async_condition_from_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> condition.ConditionCheckerType:
|
||||
"""Test a device condition."""
|
||||
platform = await async_get_device_automation_platform(
|
||||
hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION
|
||||
)
|
||||
return trace_condition_function(platform.async_condition_from_config(hass, config))
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"device": DeviceCondition,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the sun conditions."""
|
||||
return CONDITIONS
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .const import DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
|
||||
type DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]]
|
||||
|
||||
@@ -29,25 +29,9 @@ async def async_setup_entry(
|
||||
"""Set up the devolo account from a config entry."""
|
||||
mydevolo = configure_mydevolo(entry.data)
|
||||
|
||||
credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid)
|
||||
|
||||
if not credentials_valid:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
)
|
||||
|
||||
if await hass.async_add_executor_job(mydevolo.maintenance):
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="maintenance",
|
||||
)
|
||||
|
||||
gateway_ids = await hass.async_add_executor_job(mydevolo.get_gateway_ids)
|
||||
|
||||
if entry.unique_id and GATEWAY_SERIAL_PATTERN.match(entry.unique_id):
|
||||
uuid = await hass.async_add_executor_job(mydevolo.uuid)
|
||||
hass.config_entries.async_update_entry(entry, unique_id=uuid)
|
||||
gateway_ids = await hass.async_add_executor_job(
|
||||
check_mydevolo_and_get_gateway_ids, mydevolo
|
||||
)
|
||||
|
||||
def shutdown(event: Event) -> None:
|
||||
for gateway in entry.runtime_data:
|
||||
@@ -115,3 +99,19 @@ def configure_mydevolo(conf: Mapping[str, Any]) -> Mydevolo:
|
||||
mydevolo.user = conf[CONF_USERNAME]
|
||||
mydevolo.password = conf[CONF_PASSWORD]
|
||||
return mydevolo
|
||||
|
||||
|
||||
def check_mydevolo_and_get_gateway_ids(mydevolo: Mydevolo) -> list[str]:
|
||||
"""Check if the credentials are valid and return user's gateway IDs as long as mydevolo is not in maintenance mode."""
|
||||
if not mydevolo.credentials_valid():
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
)
|
||||
if mydevolo.maintenance():
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="maintenance",
|
||||
)
|
||||
|
||||
return mydevolo.get_gateway_ids()
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DevoloHomeControlConfigEntry
|
||||
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
|
||||
from .entity import DevoloMultiLevelSwitchDeviceEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Constants for the devolo_home_control integration."""
|
||||
|
||||
import re
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "devolo_home_control"
|
||||
@@ -14,5 +12,4 @@ PLATFORMS = [
|
||||
Platform.SIREN,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
GATEWAY_SERIAL_PATTERN = re.compile(r"\d{16}")
|
||||
SUPPORTED_MODEL_TYPES = ["2600", "2601"]
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DevoloHomeControlConfigEntry
|
||||
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
|
||||
from .entity import DevoloMultiLevelSwitchDeviceEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
"""Base class for multi level switches in devolo Home Control."""
|
||||
|
||||
from devolo_home_control_api.devices.zwave import Zwave
|
||||
from devolo_home_control_api.homecontrol import HomeControl
|
||||
|
||||
from .entity import DevoloDeviceEntity
|
||||
|
||||
|
||||
class DevoloMultiLevelSwitchDeviceEntity(DevoloDeviceEntity):
|
||||
"""Representation of a multi level switch device within devolo Home Control. Something like a dimmer or a thermostat."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str
|
||||
) -> None:
|
||||
"""Initialize a multi level switch within devolo Home Control."""
|
||||
super().__init__(
|
||||
homecontrol=homecontrol,
|
||||
device_instance=device_instance,
|
||||
element_uid=element_uid,
|
||||
)
|
||||
self._multi_level_switch_property = device_instance.multi_level_switch_property[
|
||||
element_uid
|
||||
]
|
||||
|
||||
self._value = self._multi_level_switch_property.value
|
||||
@@ -90,3 +90,24 @@ class DevoloDeviceEntity(Entity):
|
||||
self._attr_available = self._device_instance.is_online()
|
||||
else:
|
||||
_LOGGER.debug("No valid message received: %s", message)
|
||||
|
||||
|
||||
class DevoloMultiLevelSwitchDeviceEntity(DevoloDeviceEntity):
|
||||
"""Representation of a multi level switch device within devolo Home Control. Something like a dimmer or a thermostat."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str
|
||||
) -> None:
|
||||
"""Initialize a multi level switch within devolo Home Control."""
|
||||
super().__init__(
|
||||
homecontrol=homecontrol,
|
||||
device_instance=device_instance,
|
||||
element_uid=element_uid,
|
||||
)
|
||||
self._multi_level_switch_property = device_instance.multi_level_switch_property[
|
||||
element_uid
|
||||
]
|
||||
|
||||
self._value = self._multi_level_switch_property.value
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DevoloHomeControlConfigEntry
|
||||
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
|
||||
from .entity import DevoloMultiLevelSwitchDeviceEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DevoloHomeControlConfigEntry
|
||||
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
|
||||
from .entity import DevoloMultiLevelSwitchDeviceEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -19,6 +19,16 @@
|
||||
"password": "Password of your mydevolo account."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"username": "[%key:component::devolo_home_control::config::step::user::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::devolo_home_control::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::devolo_home_control::config::step::user::data_description::password%]"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"data": {
|
||||
"username": "[%key:component::devolo_home_control::config::step::user::data::username%]",
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyW215"],
|
||||
"requirements": ["pyW215==0.7.0"]
|
||||
"requirements": ["pyW215==0.8.0"]
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dormakaba_dkey",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["py-dormakaba-dkey==1.0.5"]
|
||||
"requirements": ["py-dormakaba-dkey==1.0.6"]
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ SENSORS: list[DROPSensorEntityDescription] = [
|
||||
native_unit_of_measurement=UnitOfVolume.GALLONS,
|
||||
suggested_display_precision=1,
|
||||
value_fn=lambda device: device.drop_api.water_used_today(),
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
DROPSensorEntityDescription(
|
||||
key=AVERAGE_WATER_USED,
|
||||
|
||||
@@ -241,6 +241,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
obis_reference="SHORT_POWER_FAILURE_COUNT",
|
||||
dsmr_versions={"2.2", "4", "5", "5L"},
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
DSMRSensorEntityDescription(
|
||||
@@ -249,6 +250,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
obis_reference="LONG_POWER_FAILURE_COUNT",
|
||||
dsmr_versions={"2.2", "4", "5", "5L"},
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
DSMRSensorEntityDescription(
|
||||
@@ -257,6 +259,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
obis_reference="VOLTAGE_SAG_L1_COUNT",
|
||||
dsmr_versions={"2.2", "4", "5", "5L"},
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
DSMRSensorEntityDescription(
|
||||
@@ -265,6 +268,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
obis_reference="VOLTAGE_SAG_L2_COUNT",
|
||||
dsmr_versions={"2.2", "4", "5", "5L"},
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
DSMRSensorEntityDescription(
|
||||
@@ -273,6 +277,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
obis_reference="VOLTAGE_SAG_L3_COUNT",
|
||||
dsmr_versions={"2.2", "4", "5", "5L"},
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
DSMRSensorEntityDescription(
|
||||
@@ -281,6 +286,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
obis_reference="VOLTAGE_SWELL_L1_COUNT",
|
||||
dsmr_versions={"2.2", "4", "5", "5L"},
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
DSMRSensorEntityDescription(
|
||||
@@ -289,6 +295,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
obis_reference="VOLTAGE_SWELL_L2_COUNT",
|
||||
dsmr_versions={"2.2", "4", "5", "5L"},
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
DSMRSensorEntityDescription(
|
||||
@@ -297,6 +304,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
obis_reference="VOLTAGE_SWELL_L3_COUNT",
|
||||
dsmr_versions={"2.2", "4", "5", "5L"},
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
DSMRSensorEntityDescription(
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
"""Support for sending data to Dweet.io."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import dweepy
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
CONF_NAME,
|
||||
CONF_WHITELIST,
|
||||
EVENT_STATE_CHANGED,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, state as state_helper
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "dweet"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_WHITELIST, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.entity_id]
|
||||
),
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Dweet.io component."""
|
||||
conf = config[DOMAIN]
|
||||
name = conf.get(CONF_NAME)
|
||||
whitelist = conf.get(CONF_WHITELIST)
|
||||
json_body = {}
|
||||
|
||||
def dweet_event_listener(event):
|
||||
"""Listen for new messages on the bus and sends them to Dweet.io."""
|
||||
state = event.data.get("new_state")
|
||||
if (
|
||||
state is None
|
||||
or state.state in (STATE_UNKNOWN, "")
|
||||
or state.entity_id not in whitelist
|
||||
):
|
||||
return
|
||||
|
||||
try:
|
||||
_state = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
_state = state.state
|
||||
|
||||
json_body[state.attributes.get(ATTR_FRIENDLY_NAME)] = _state
|
||||
|
||||
send_data(name, json_body)
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, dweet_event_listener)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def send_data(name, msg):
|
||||
"""Send the collected data to Dweet.io."""
|
||||
try:
|
||||
dweepy.dweet_for(name, msg)
|
||||
except dweepy.DweepyError:
|
||||
_LOGGER.error("Error saving data to Dweet.io: %s", msg)
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"domain": "dweet",
|
||||
"name": "dweet.io",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/dweet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["dweepy"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["dweepy==0.3.0"]
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
"""Support for showing values from Dweet.io."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import json
|
||||
import logging
|
||||
|
||||
import dweepy
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE,
|
||||
CONF_NAME,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Dweet.io Sensor"
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_DEVICE): cv.string,
|
||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dweet sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
device = config.get(CONF_DEVICE)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
|
||||
try:
|
||||
content = json.dumps(dweepy.get_latest_dweet_for(device)[0]["content"])
|
||||
except dweepy.DweepyError:
|
||||
_LOGGER.error("Device/thing %s could not be found", device)
|
||||
return
|
||||
|
||||
if value_template and value_template.render_with_possible_json_value(content) == "":
|
||||
_LOGGER.error("%s was not found", value_template)
|
||||
return
|
||||
|
||||
dweet = DweetData(device)
|
||||
|
||||
add_entities([DweetSensor(hass, dweet, name, value_template, unit)], True)
|
||||
|
||||
|
||||
class DweetSensor(SensorEntity):
|
||||
"""Representation of a Dweet sensor."""
|
||||
|
||||
def __init__(self, hass, dweet, name, value_template, unit_of_measurement):
|
||||
"""Initialize the sensor."""
|
||||
self.hass = hass
|
||||
self.dweet = dweet
|
||||
self._name = name
|
||||
self._value_template = value_template
|
||||
self._state = None
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state."""
|
||||
return self._state
|
||||
|
||||
def update(self) -> None:
|
||||
"""Get the latest data from REST API."""
|
||||
self.dweet.update()
|
||||
|
||||
if self.dweet.data is None:
|
||||
self._state = None
|
||||
else:
|
||||
values = json.dumps(self.dweet.data[0]["content"])
|
||||
self._state = self._value_template.render_with_possible_json_value(
|
||||
values, None
|
||||
)
|
||||
|
||||
|
||||
class DweetData:
|
||||
"""The class for handling the data retrieval."""
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize the sensor."""
|
||||
self._device = device
|
||||
self.data = None
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from Dweet.io."""
|
||||
try:
|
||||
self.data = dweepy.get_latest_dweet_for(self._device)
|
||||
except dweepy.DweepyError:
|
||||
_LOGGER.warning("Device %s doesn't contain any data", self._device)
|
||||
self.data = None
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
|
||||
"/ivp/ensemble/generator",
|
||||
"/ivp/meters",
|
||||
"/ivp/meters/readings",
|
||||
"/ivp/pdm/device_data",
|
||||
"/home",
|
||||
]
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==2.0.1"],
|
||||
"requirements": ["pyenphase==2.1.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -45,6 +45,7 @@ from homeassistant.const import (
|
||||
UnitOfFrequency,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -80,6 +81,114 @@ INVERTER_SENSORS = (
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
value_fn=attrgetter("last_report_watts"),
|
||||
),
|
||||
EnvoyInverterSensorEntityDescription(
|
||||
key="dc_voltage",
|
||||
translation_key="dc_voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
suggested_display_precision=3,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("dc_voltage"),
|
||||
),
|
||||
EnvoyInverterSensorEntityDescription(
|
||||
key="dc_current",
|
||||
translation_key="dc_current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
suggested_display_precision=3,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("dc_current"),
|
||||
),
|
||||
EnvoyInverterSensorEntityDescription(
|
||||
key="ac_voltage",
|
||||
translation_key="ac_voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
suggested_display_precision=3,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("ac_voltage"),
|
||||
),
|
||||
EnvoyInverterSensorEntityDescription(
|
||||
key="ac_current",
|
||||
translation_key="ac_current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
suggested_display_precision=3,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("ac_current"),
|
||||
),
|
||||
EnvoyInverterSensorEntityDescription(
|
||||
key="ac_frequency",
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
suggested_display_precision=3,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("ac_frequency"),
|
||||
),
|
||||
EnvoyInverterSensorEntityDescription(
|
||||
key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
suggested_display_precision=3,
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=attrgetter("temperature"),
|
||||
),
|
||||
EnvoyInverterSensorEntityDescription(
|
||||
key="lifetime_energy",
|
||||
translation_key="lifetime_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("lifetime_energy"),
|
||||
),
|
||||
EnvoyInverterSensorEntityDescription(
|
||||
key="energy_today",
|
||||
translation_key="energy_today",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("energy_today"),
|
||||
),
|
||||
EnvoyInverterSensorEntityDescription(
|
||||
key="last_report_duration",
|
||||
translation_key="last_report_duration",
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=attrgetter("last_report_duration"),
|
||||
),
|
||||
EnvoyInverterSensorEntityDescription(
|
||||
key="energy_produced",
|
||||
translation_key="energy_produced",
|
||||
native_unit_of_measurement=UnitOfEnergy.MILLIWATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
suggested_display_precision=3,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=attrgetter("energy_produced"),
|
||||
),
|
||||
EnvoyInverterSensorEntityDescription(
|
||||
key="max_reported",
|
||||
translation_key="max_reported",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=attrgetter("max_report_watts"),
|
||||
),
|
||||
EnvoyInverterSensorEntityDescription(
|
||||
key=LAST_REPORTED_KEY,
|
||||
translation_key=LAST_REPORTED_KEY,
|
||||
|
||||
@@ -379,7 +379,34 @@
|
||||
"name": "Aggregated Battery capacity"
|
||||
},
|
||||
"aggregated_soc": {
|
||||
"name": "Aggregated battery soc"
|
||||
"name": "Aggregated battery SOC"
|
||||
},
|
||||
"dc_voltage": {
|
||||
"name": "DC voltage"
|
||||
},
|
||||
"dc_current": {
|
||||
"name": "DC current"
|
||||
},
|
||||
"ac_voltage": {
|
||||
"name": "AC voltage"
|
||||
},
|
||||
"ac_current": {
|
||||
"name": "AC current"
|
||||
},
|
||||
"lifetime_energy": {
|
||||
"name": "[%key:component::enphase_envoy::entity::sensor::lifetime_production::name%]"
|
||||
},
|
||||
"energy_today": {
|
||||
"name": "[%key:component::enphase_envoy::entity::sensor::daily_production::name%]"
|
||||
},
|
||||
"energy_produced": {
|
||||
"name": "Energy production since previous report"
|
||||
},
|
||||
"max_reported": {
|
||||
"name": "Lifetime maximum power"
|
||||
},
|
||||
"last_report_duration": {
|
||||
"name": "Last report duration"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
|
||||
@@ -284,11 +284,15 @@ class EsphomeAssistSatellite(
|
||||
assert event.data is not None
|
||||
data_to_send = {"text": event.data["stt_output"]["text"]}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS:
|
||||
data_to_send = {
|
||||
"tts_start_streaming": "1"
|
||||
if (event.data and event.data.get("tts_start_streaming"))
|
||||
else "0",
|
||||
}
|
||||
if (
|
||||
not event.data
|
||||
or ("tts_start_streaming" not in event.data)
|
||||
or (not event.data["tts_start_streaming"])
|
||||
):
|
||||
# ESPHome only needs to know if early TTS streaming is available
|
||||
return
|
||||
|
||||
data_to_send = {"tts_start_streaming": "1"}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END:
|
||||
assert event.data is not None
|
||||
data_to_send = {
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
import functools
|
||||
import logging
|
||||
import math
|
||||
from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeVar, cast
|
||||
|
||||
@@ -13,7 +14,6 @@ from aioesphomeapi import (
|
||||
EntityCategory as EsphomeEntityCategory,
|
||||
EntityInfo,
|
||||
EntityState,
|
||||
build_unique_id,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -24,6 +24,7 @@ from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_platform,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -32,9 +33,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from .const import DOMAIN
|
||||
|
||||
# Import config flow so that it's added to the registry
|
||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData, build_device_unique_id
|
||||
from .enum_mapper import EsphomeEnumMapper
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_InfoT = TypeVar("_InfoT", bound=EntityInfo)
|
||||
_EntityT = TypeVar("_EntityT", bound="EsphomeEntity[Any,Any]")
|
||||
_StateT = TypeVar("_StateT", bound=EntityState)
|
||||
@@ -53,21 +56,74 @@ def async_static_info_updated(
|
||||
) -> None:
|
||||
"""Update entities of this platform when entities are listed."""
|
||||
current_infos = entry_data.info[info_type]
|
||||
device_info = entry_data.device_info
|
||||
if TYPE_CHECKING:
|
||||
assert device_info is not None
|
||||
new_infos: dict[int, EntityInfo] = {}
|
||||
add_entities: list[_EntityT] = []
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
dev_reg = dr.async_get(hass)
|
||||
|
||||
for info in infos:
|
||||
if not current_infos.pop(info.key, None):
|
||||
# Create new entity
|
||||
new_infos[info.key] = info
|
||||
|
||||
# Create new entity if it doesn't exist
|
||||
if not (old_info := current_infos.pop(info.key, None)):
|
||||
entity = entity_type(entry_data, platform.domain, info, state_type)
|
||||
add_entities.append(entity)
|
||||
new_infos[info.key] = info
|
||||
continue
|
||||
|
||||
# Entity exists - check if device_id has changed
|
||||
if old_info.device_id == info.device_id:
|
||||
continue
|
||||
|
||||
# Entity has switched devices, need to migrate unique_id
|
||||
old_unique_id = build_device_unique_id(device_info.mac_address, old_info)
|
||||
entity_id = ent_reg.async_get_entity_id(platform.domain, DOMAIN, old_unique_id)
|
||||
|
||||
# If entity not found in registry, re-add it
|
||||
# This happens when the device_id changed and the old device was deleted
|
||||
if entity_id is None:
|
||||
_LOGGER.info(
|
||||
"Entity with old unique_id %s not found in registry after device_id "
|
||||
"changed from %s to %s, re-adding entity",
|
||||
old_unique_id,
|
||||
old_info.device_id,
|
||||
info.device_id,
|
||||
)
|
||||
entity = entity_type(entry_data, platform.domain, info, state_type)
|
||||
add_entities.append(entity)
|
||||
continue
|
||||
|
||||
updates: dict[str, Any] = {}
|
||||
new_unique_id = build_device_unique_id(device_info.mac_address, info)
|
||||
|
||||
# Update unique_id if it changed
|
||||
if old_unique_id != new_unique_id:
|
||||
updates["new_unique_id"] = new_unique_id
|
||||
|
||||
# Update device assignment
|
||||
if info.device_id:
|
||||
# Entity now belongs to a sub device
|
||||
new_device = dev_reg.async_get_device(
|
||||
identifiers={(DOMAIN, f"{device_info.mac_address}_{info.device_id}")}
|
||||
)
|
||||
else:
|
||||
# Entity now belongs to the main device
|
||||
new_device = dev_reg.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||
)
|
||||
|
||||
if new_device:
|
||||
updates["device_id"] = new_device.id
|
||||
|
||||
# Apply all updates at once
|
||||
if updates:
|
||||
ent_reg.async_update_entity(entity_id, **updates)
|
||||
|
||||
# Anything still in current_infos is now gone
|
||||
if current_infos:
|
||||
device_info = entry_data.device_info
|
||||
if TYPE_CHECKING:
|
||||
assert device_info is not None
|
||||
entry_data.async_remove_entities(
|
||||
hass, current_infos.values(), device_info.mac_address
|
||||
)
|
||||
@@ -225,7 +281,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool
|
||||
_has_state: bool = False
|
||||
unique_id: str
|
||||
|
||||
def __init__(
|
||||
@@ -244,11 +300,28 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
self._key = entity_info.key
|
||||
self._state_type = state_type
|
||||
self._on_static_info_update(entity_info)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||
)
|
||||
|
||||
device_name = device_info.name
|
||||
# Determine the device connection based on whether this entity belongs to a sub device
|
||||
if entity_info.device_id:
|
||||
# Entity belongs to a sub device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, f"{device_info.mac_address}_{entity_info.device_id}")
|
||||
}
|
||||
)
|
||||
# Use the pre-computed device_id_to_name mapping for O(1) lookup
|
||||
device_name = entry_data.device_id_to_name.get(
|
||||
entity_info.device_id, device_info.name
|
||||
)
|
||||
else:
|
||||
# Entity belongs to the main device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||
)
|
||||
|
||||
if entity_info.name:
|
||||
self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}"
|
||||
self.entity_id = f"{domain}.{device_name}_{entity_info.name}"
|
||||
else:
|
||||
# https://github.com/home-assistant/core/issues/132532
|
||||
# If name is not set, ESPHome will use the sanitized friendly name
|
||||
@@ -256,7 +329,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
# as the entity_id before it is sanitized since the sanitizer
|
||||
# is not utf-8 aware. In this case, its always going to be
|
||||
# an empty string so we drop the object_id.
|
||||
self.entity_id = f"{domain}.{device_info.name}"
|
||||
self.entity_id = f"{domain}.{device_name}"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
@@ -290,7 +363,9 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
static_info = cast(_InfoT, static_info)
|
||||
assert device_info
|
||||
self._static_info = static_info
|
||||
self._attr_unique_id = build_unique_id(device_info.mac_address, static_info)
|
||||
self._attr_unique_id = build_device_unique_id(
|
||||
device_info.mac_address, static_info
|
||||
)
|
||||
self._attr_entity_registry_enabled_default = not static_info.disabled_by_default
|
||||
# https://github.com/home-assistant/core/issues/132532
|
||||
# If the name is "", we need to set it to None since otherwise
|
||||
|
||||
@@ -95,6 +95,22 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = {
|
||||
}
|
||||
|
||||
|
||||
def build_device_unique_id(mac: str, entity_info: EntityInfo) -> str:
|
||||
"""Build unique ID for entity, appending @device_id if it belongs to a sub-device.
|
||||
|
||||
This wrapper around build_unique_id ensures that entities belonging to sub-devices
|
||||
have their device_id appended to the unique_id to handle proper migration when
|
||||
entities move between devices.
|
||||
"""
|
||||
base_unique_id = build_unique_id(mac, entity_info)
|
||||
|
||||
# If entity belongs to a sub-device, append @device_id
|
||||
if entity_info.device_id:
|
||||
return f"{base_unique_id}@{entity_info.device_id}"
|
||||
|
||||
return base_unique_id
|
||||
|
||||
|
||||
class StoreData(TypedDict, total=False):
|
||||
"""ESPHome storage data."""
|
||||
|
||||
@@ -160,6 +176,7 @@ class RuntimeEntryData:
|
||||
assist_satellite_set_wake_word_callbacks: list[Callable[[str], None]] = field(
|
||||
default_factory=list
|
||||
)
|
||||
device_id_to_name: dict[int, str] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -222,7 +239,9 @@ class RuntimeEntryData:
|
||||
ent_reg = er.async_get(hass)
|
||||
for info in static_infos:
|
||||
if entry := ent_reg.async_get_entity_id(
|
||||
INFO_TYPE_TO_PLATFORM[type(info)], DOMAIN, build_unique_id(mac, info)
|
||||
INFO_TYPE_TO_PLATFORM[type(info)],
|
||||
DOMAIN,
|
||||
build_device_unique_id(mac, info),
|
||||
):
|
||||
ent_reg.async_remove(entry)
|
||||
|
||||
@@ -278,7 +297,8 @@ class RuntimeEntryData:
|
||||
if (
|
||||
(old_unique_id := info.unique_id)
|
||||
and (old_entry := registry_get_entity(platform, DOMAIN, old_unique_id))
|
||||
and (new_unique_id := build_unique_id(mac, info)) != old_unique_id
|
||||
and (new_unique_id := build_device_unique_id(mac, info))
|
||||
!= old_unique_id
|
||||
and not registry_get_entity(platform, DOMAIN, new_unique_id)
|
||||
):
|
||||
ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id)
|
||||
|
||||
@@ -527,6 +527,11 @@ class ESPHomeManager:
|
||||
device_info.name,
|
||||
device_mac,
|
||||
)
|
||||
# Build device_id_to_name mapping for efficient lookup
|
||||
entry_data.device_id_to_name = {
|
||||
sub_device.device_id: sub_device.name or device_info.name
|
||||
for sub_device in device_info.devices
|
||||
}
|
||||
self.device_id = _async_setup_device_registry(hass, entry, entry_data)
|
||||
|
||||
entry_data.async_update_device_state()
|
||||
@@ -751,6 +756,28 @@ def _async_setup_device_registry(
|
||||
device_info = entry_data.device_info
|
||||
if TYPE_CHECKING:
|
||||
assert device_info is not None
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
# Build sets of valid device identifiers and connections
|
||||
valid_connections = {
|
||||
(dr.CONNECTION_NETWORK_MAC, format_mac(device_info.mac_address))
|
||||
}
|
||||
valid_identifiers = {
|
||||
(DOMAIN, f"{device_info.mac_address}_{sub_device.device_id}")
|
||||
for sub_device in device_info.devices
|
||||
}
|
||||
|
||||
# Remove devices that no longer exist
|
||||
for device in dr.async_entries_for_config_entry(device_registry, entry.entry_id):
|
||||
# Skip devices we want to keep
|
||||
if (
|
||||
device.connections & valid_connections
|
||||
or device.identifiers & valid_identifiers
|
||||
):
|
||||
continue
|
||||
# Remove everything else
|
||||
device_registry.async_remove_device(device.id)
|
||||
|
||||
sw_version = device_info.esphome_version
|
||||
if device_info.compilation_time:
|
||||
sw_version += f" ({device_info.compilation_time})"
|
||||
@@ -779,11 +806,14 @@ def _async_setup_device_registry(
|
||||
f"{device_info.project_version} (ESPHome {device_info.esphome_version})"
|
||||
)
|
||||
|
||||
suggested_area = None
|
||||
if device_info.suggested_area:
|
||||
suggested_area: str | None = None
|
||||
if device_info.area and device_info.area.name:
|
||||
# Prefer device_info.area over suggested_area when area name is not empty
|
||||
suggested_area = device_info.area.name
|
||||
elif device_info.suggested_area:
|
||||
suggested_area = device_info.suggested_area
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
# Create/update main device
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
configuration_url=configuration_url,
|
||||
@@ -794,6 +824,36 @@ def _async_setup_device_registry(
|
||||
sw_version=sw_version,
|
||||
suggested_area=suggested_area,
|
||||
)
|
||||
|
||||
# Handle sub devices
|
||||
# Find available areas from device_info
|
||||
areas_by_id = {area.area_id: area for area in device_info.areas}
|
||||
# Add the main device's area if it exists
|
||||
if device_info.area:
|
||||
areas_by_id[device_info.area.area_id] = device_info.area
|
||||
# Create/update sub devices that should exist
|
||||
for sub_device in device_info.devices:
|
||||
# Determine the area for this sub device
|
||||
sub_device_suggested_area: str | None = None
|
||||
if sub_device.area_id is not None and sub_device.area_id in areas_by_id:
|
||||
sub_device_suggested_area = areas_by_id[sub_device.area_id].name
|
||||
|
||||
sub_device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, f"{device_info.mac_address}_{sub_device.device_id}")},
|
||||
name=sub_device.name or device_entry.name,
|
||||
manufacturer=manufacturer,
|
||||
model=model,
|
||||
sw_version=sw_version,
|
||||
suggested_area=sub_device_suggested_area,
|
||||
)
|
||||
|
||||
# Update the sub device to set via_device_id
|
||||
device_registry.async_update_device(
|
||||
sub_device_entry.id,
|
||||
via_device_id=device_entry.id,
|
||||
)
|
||||
|
||||
return device_entry.id
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==32.2.4",
|
||||
"aioesphomeapi==33.1.1",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
|
||||
@@ -81,6 +81,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
||||
# if the string is empty
|
||||
if unit_of_measurement := static_info.unit_of_measurement:
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
self._attr_suggested_display_precision = static_info.accuracy_decimals
|
||||
self._attr_device_class = try_parse_enum(
|
||||
SensorDeviceClass, static_info.device_class
|
||||
)
|
||||
@@ -97,7 +98,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
||||
self._attr_state_class = _STATE_CLASSES.from_esphome(state_class)
|
||||
|
||||
@property
|
||||
def native_value(self) -> datetime | str | None:
|
||||
def native_value(self) -> datetime | int | float | None:
|
||||
"""Return the state of the entity."""
|
||||
if not self._has_state or (state := self._state).missing_state:
|
||||
return None
|
||||
@@ -106,7 +107,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
||||
return None
|
||||
if self.device_class is SensorDeviceClass.TIMESTAMP:
|
||||
return dt_util.utc_from_timestamp(state_float)
|
||||
return f"{state_float:.{self._static_info.accuracy_decimals}f}"
|
||||
return state_float
|
||||
|
||||
|
||||
class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity):
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from pyezvizapi.constants import DeviceSwitchType, SoundMode
|
||||
from pyezvizapi.constants import (
|
||||
BatteryCameraWorkMode,
|
||||
DeviceCatagories,
|
||||
DeviceSwitchType,
|
||||
SoundMode,
|
||||
SupportExt,
|
||||
)
|
||||
from pyezvizapi.exceptions import HTTPError, PyEzvizError
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
@@ -24,17 +32,83 @@ class EzvizSelectEntityDescription(SelectEntityDescription):
|
||||
"""Describe a EZVIZ Select entity."""
|
||||
|
||||
supported_switch: int
|
||||
current_option: Callable[[EzvizSelect], str | None]
|
||||
select_option: Callable[[EzvizSelect, str, str], None]
|
||||
|
||||
|
||||
SELECT_TYPE = EzvizSelectEntityDescription(
|
||||
def alarm_sound_mode_current_option(ezvizSelect: EzvizSelect) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
sound_mode_value = getattr(
|
||||
SoundMode, ezvizSelect.data[ezvizSelect.entity_description.key]
|
||||
).value
|
||||
if sound_mode_value in [0, 1, 2]:
|
||||
return ezvizSelect.options[sound_mode_value]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def alarm_sound_mode_select_option(
|
||||
ezvizSelect: EzvizSelect, serial: str, option: str
|
||||
) -> None:
|
||||
"""Change the selected option."""
|
||||
sound_mode_value = ezvizSelect.options.index(option)
|
||||
ezvizSelect.coordinator.ezviz_client.alarm_sound(serial, sound_mode_value, 1)
|
||||
|
||||
|
||||
ALARM_SOUND_MODE_SELECT_TYPE = EzvizSelectEntityDescription(
|
||||
key="alarm_sound_mod",
|
||||
translation_key="alarm_sound_mode",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
options=["soft", "intensive", "silent"],
|
||||
supported_switch=DeviceSwitchType.ALARM_TONE.value,
|
||||
current_option=alarm_sound_mode_current_option,
|
||||
select_option=alarm_sound_mode_select_option,
|
||||
)
|
||||
|
||||
|
||||
def battery_work_mode_current_option(ezvizSelect: EzvizSelect) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
battery_work_mode = getattr(
|
||||
BatteryCameraWorkMode,
|
||||
ezvizSelect.data[ezvizSelect.entity_description.key],
|
||||
BatteryCameraWorkMode.UNKNOWN,
|
||||
)
|
||||
if battery_work_mode == BatteryCameraWorkMode.UNKNOWN:
|
||||
return None
|
||||
|
||||
return battery_work_mode.name.lower()
|
||||
|
||||
|
||||
def battery_work_mode_select_option(
|
||||
ezvizSelect: EzvizSelect, serial: str, option: str
|
||||
) -> None:
|
||||
"""Change the selected option."""
|
||||
battery_work_mode = getattr(BatteryCameraWorkMode, option.upper())
|
||||
ezvizSelect.coordinator.ezviz_client.set_battery_camera_work_mode(
|
||||
serial, battery_work_mode.value
|
||||
)
|
||||
|
||||
|
||||
BATTERY_WORK_MODE_SELECT_TYPE = EzvizSelectEntityDescription(
|
||||
key="battery_camera_work_mode",
|
||||
translation_key="battery_camera_work_mode",
|
||||
icon="mdi:battery-sync",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
options=[
|
||||
"plugged_in",
|
||||
"high_performance",
|
||||
"power_save",
|
||||
"super_power_save",
|
||||
"custom",
|
||||
],
|
||||
supported_switch=-1,
|
||||
current_option=battery_work_mode_current_option,
|
||||
select_option=battery_work_mode_select_option,
|
||||
)
|
||||
|
||||
SELECT_TYPES = [ALARM_SOUND_MODE_SELECT_TYPE, BATTERY_WORK_MODE_SELECT_TYPE]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: EzvizConfigEntry,
|
||||
@@ -43,12 +117,26 @@ async def async_setup_entry(
|
||||
"""Set up EZVIZ select entities based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
EzvizSelect(coordinator, camera)
|
||||
entities = [
|
||||
EzvizSelect(coordinator, camera, ALARM_SOUND_MODE_SELECT_TYPE)
|
||||
for camera in coordinator.data
|
||||
for switch in coordinator.data[camera]["switches"]
|
||||
if switch == SELECT_TYPE.supported_switch
|
||||
)
|
||||
if switch == ALARM_SOUND_MODE_SELECT_TYPE.supported_switch
|
||||
]
|
||||
|
||||
for camera in coordinator.data:
|
||||
device_category = coordinator.data[camera].get("device_category")
|
||||
supportExt = coordinator.data[camera].get("supportExt")
|
||||
if (
|
||||
device_category == DeviceCatagories.BATTERY_CAMERA_DEVICE_CATEGORY.value
|
||||
and supportExt
|
||||
and str(SupportExt.SupportBatteryManage.value) in supportExt
|
||||
):
|
||||
entities.append(
|
||||
EzvizSelect(coordinator, camera, BATTERY_WORK_MODE_SELECT_TYPE)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class EzvizSelect(EzvizEntity, SelectEntity):
|
||||
@@ -58,31 +146,23 @@ class EzvizSelect(EzvizEntity, SelectEntity):
|
||||
self,
|
||||
coordinator: EzvizDataUpdateCoordinator,
|
||||
serial: str,
|
||||
description: EzvizSelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
"""Initialize the select entity."""
|
||||
super().__init__(coordinator, serial)
|
||||
self._attr_unique_id = f"{serial}_{SELECT_TYPE.key}"
|
||||
self.entity_description = SELECT_TYPE
|
||||
self._attr_unique_id = f"{serial}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
sound_mode_value = getattr(
|
||||
SoundMode, self.data[self.entity_description.key]
|
||||
).value
|
||||
if sound_mode_value in [0, 1, 2]:
|
||||
return self.options[sound_mode_value]
|
||||
|
||||
return None
|
||||
desc = cast(EzvizSelectEntityDescription, self.entity_description)
|
||||
return desc.current_option(self)
|
||||
|
||||
def select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
sound_mode_value = self.options.index(option)
|
||||
|
||||
desc = cast(EzvizSelectEntityDescription, self.entity_description)
|
||||
try:
|
||||
self.coordinator.ezviz_client.alarm_sound(self._serial, sound_mode_value, 1)
|
||||
|
||||
return desc.select_option(self, self._serial, option)
|
||||
except (HTTPError, PyEzvizError) as err:
|
||||
raise HomeAssistantError(
|
||||
f"Cannot set Warning sound level for {self.entity_id}"
|
||||
) from err
|
||||
raise HomeAssistantError(f"Cannot select option for {desc.key}") from err
|
||||
|
||||
@@ -68,6 +68,16 @@
|
||||
"intensive": "Intensive",
|
||||
"silent": "Silent"
|
||||
}
|
||||
},
|
||||
"battery_camera_work_mode": {
|
||||
"name": "Battery work mode",
|
||||
"state": {
|
||||
"plugged_in": "Plugged in",
|
||||
"high_performance": "High performance",
|
||||
"power_save": "Power save",
|
||||
"super_power_save": "Super power saving",
|
||||
"custom": "Custom"
|
||||
}
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""The foscam component."""
|
||||
|
||||
from libpyfoscam import FoscamCamera
|
||||
from libpyfoscamcgi import FoscamCamera
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from libpyfoscam import FoscamCamera
|
||||
from libpyfoscam.foscam import (
|
||||
from libpyfoscamcgi import FoscamCamera
|
||||
from libpyfoscamcgi.foscamcgi import (
|
||||
ERROR_FOSCAM_AUTH,
|
||||
ERROR_FOSCAM_UNAVAILABLE,
|
||||
FOSCAM_SUCCESS,
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from libpyfoscam import FoscamCamera
|
||||
from libpyfoscamcgi import FoscamCamera
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/foscam",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["libpyfoscam"],
|
||||
"requirements": ["libpyfoscam==1.2.2"]
|
||||
"loggers": ["libpyfoscamcgi"],
|
||||
"requirements": ["libpyfoscamcgi==0.0.6"]
|
||||
}
|
||||
|
||||
@@ -15,8 +15,9 @@ from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import ConnectionInfo, FritzConfigEntry
|
||||
from .coordinator import FritzConfigEntry
|
||||
from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
|
||||
from .models import ConnectionInfo
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -19,15 +19,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import BUTTON_TYPE_WOL, CONNECTION_TYPE_LAN, MeshRoles
|
||||
from .coordinator import (
|
||||
FRITZ_DATA_KEY,
|
||||
AvmWrapper,
|
||||
FritzConfigEntry,
|
||||
FritzData,
|
||||
FritzDevice,
|
||||
_is_tracked,
|
||||
)
|
||||
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
|
||||
from .entity import FritzDeviceBase
|
||||
from .helpers import _is_tracked
|
||||
from .models import FritzDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping, ValuesView
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
@@ -34,7 +34,6 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
@@ -48,6 +47,15 @@ from .const import (
|
||||
FRITZ_EXCEPTIONS,
|
||||
MeshRoles,
|
||||
)
|
||||
from .helpers import _ha_is_stopping
|
||||
from .models import (
|
||||
ConnectionInfo,
|
||||
Device,
|
||||
FritzDevice,
|
||||
HostAttributes,
|
||||
HostInfo,
|
||||
Interface,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -56,33 +64,13 @@ FRITZ_DATA_KEY: HassKey[FritzData] = HassKey(DOMAIN)
|
||||
type FritzConfigEntry = ConfigEntry[AvmWrapper]
|
||||
|
||||
|
||||
def _is_tracked(mac: str, current_devices: ValuesView[set[str]]) -> bool:
|
||||
"""Check if device is already tracked."""
|
||||
return any(mac in tracked for tracked in current_devices)
|
||||
@dataclass
|
||||
class FritzData:
|
||||
"""Storage class for platform global data."""
|
||||
|
||||
|
||||
def device_filter_out_from_trackers(
|
||||
mac: str,
|
||||
device: FritzDevice,
|
||||
current_devices: ValuesView[set[str]],
|
||||
) -> bool:
|
||||
"""Check if device should be filtered out from trackers."""
|
||||
reason: str | None = None
|
||||
if device.ip_address == "":
|
||||
reason = "Missing IP"
|
||||
elif _is_tracked(mac, current_devices):
|
||||
reason = "Already tracked"
|
||||
|
||||
if reason:
|
||||
_LOGGER.debug(
|
||||
"Skip adding device %s [%s], reason: %s", device.hostname, mac, reason
|
||||
)
|
||||
return bool(reason)
|
||||
|
||||
|
||||
def _ha_is_stopping(activity: str) -> None:
|
||||
"""Inform that HA is stopping."""
|
||||
_LOGGER.warning("Cannot execute %s: HomeAssistant is shutting down", activity)
|
||||
tracked: dict[str, set[str]] = field(default_factory=dict)
|
||||
profile_switches: dict[str, set[str]] = field(default_factory=dict)
|
||||
wol_buttons: dict[str, set[str]] = field(default_factory=dict)
|
||||
|
||||
|
||||
class ClassSetupMissing(Exception):
|
||||
@@ -93,68 +81,6 @@ class ClassSetupMissing(Exception):
|
||||
super().__init__("Function called before Class setup")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Device:
|
||||
"""FRITZ!Box device class."""
|
||||
|
||||
connected: bool
|
||||
connected_to: str
|
||||
connection_type: str
|
||||
ip_address: str
|
||||
name: str
|
||||
ssid: str | None
|
||||
wan_access: bool | None = None
|
||||
|
||||
|
||||
class Interface(TypedDict):
|
||||
"""Interface details."""
|
||||
|
||||
device: str
|
||||
mac: str
|
||||
op_mode: str
|
||||
ssid: str | None
|
||||
type: str
|
||||
|
||||
|
||||
HostAttributes = TypedDict(
|
||||
"HostAttributes",
|
||||
{
|
||||
"Index": int,
|
||||
"IPAddress": str,
|
||||
"MACAddress": str,
|
||||
"Active": bool,
|
||||
"HostName": str,
|
||||
"InterfaceType": str,
|
||||
"X_AVM-DE_Port": int,
|
||||
"X_AVM-DE_Speed": int,
|
||||
"X_AVM-DE_UpdateAvailable": bool,
|
||||
"X_AVM-DE_UpdateSuccessful": str,
|
||||
"X_AVM-DE_InfoURL": str | None,
|
||||
"X_AVM-DE_MACAddressList": str | None,
|
||||
"X_AVM-DE_Model": str | None,
|
||||
"X_AVM-DE_URL": str | None,
|
||||
"X_AVM-DE_Guest": bool,
|
||||
"X_AVM-DE_RequestClient": str,
|
||||
"X_AVM-DE_VPN": bool,
|
||||
"X_AVM-DE_WANAccess": str,
|
||||
"X_AVM-DE_Disallow": bool,
|
||||
"X_AVM-DE_IsMeshable": str,
|
||||
"X_AVM-DE_Priority": str,
|
||||
"X_AVM-DE_FriendlyName": str,
|
||||
"X_AVM-DE_FriendlyNameIsWriteable": str,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class HostInfo(TypedDict):
|
||||
"""FRITZ!Box host info class."""
|
||||
|
||||
mac: str
|
||||
name: str
|
||||
ip: str
|
||||
status: bool
|
||||
|
||||
|
||||
class UpdateCoordinatorDataType(TypedDict):
|
||||
"""Update coordinator data type."""
|
||||
|
||||
@@ -898,120 +824,3 @@ class AvmWrapper(FritzBoxTools):
|
||||
"X_AVM-DE_WakeOnLANByMACAddress",
|
||||
NewMACAddress=mac_address,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FritzData:
|
||||
"""Storage class for platform global data."""
|
||||
|
||||
tracked: dict[str, set[str]] = field(default_factory=dict)
|
||||
profile_switches: dict[str, set[str]] = field(default_factory=dict)
|
||||
wol_buttons: dict[str, set[str]] = field(default_factory=dict)
|
||||
|
||||
|
||||
class FritzDevice:
|
||||
"""Representation of a device connected to the FRITZ!Box."""
|
||||
|
||||
def __init__(self, mac: str, name: str) -> None:
|
||||
"""Initialize device info."""
|
||||
self._connected = False
|
||||
self._connected_to: str | None = None
|
||||
self._connection_type: str | None = None
|
||||
self._ip_address: str | None = None
|
||||
self._last_activity: datetime | None = None
|
||||
self._mac = mac
|
||||
self._name = name
|
||||
self._ssid: str | None = None
|
||||
self._wan_access: bool | None = False
|
||||
|
||||
def update(self, dev_info: Device, consider_home: float) -> None:
|
||||
"""Update device info."""
|
||||
utc_point_in_time = dt_util.utcnow()
|
||||
|
||||
if self._last_activity:
|
||||
consider_home_evaluated = (
|
||||
utc_point_in_time - self._last_activity
|
||||
).total_seconds() < consider_home
|
||||
else:
|
||||
consider_home_evaluated = dev_info.connected
|
||||
|
||||
if not self._name:
|
||||
self._name = dev_info.name or self._mac.replace(":", "_")
|
||||
|
||||
self._connected = dev_info.connected or consider_home_evaluated
|
||||
|
||||
if dev_info.connected:
|
||||
self._last_activity = utc_point_in_time
|
||||
|
||||
self._connected_to = dev_info.connected_to
|
||||
self._connection_type = dev_info.connection_type
|
||||
self._ip_address = dev_info.ip_address
|
||||
self._ssid = dev_info.ssid
|
||||
self._wan_access = dev_info.wan_access
|
||||
|
||||
@property
|
||||
def connected_to(self) -> str | None:
|
||||
"""Return connected status."""
|
||||
return self._connected_to
|
||||
|
||||
@property
|
||||
def connection_type(self) -> str | None:
|
||||
"""Return connected status."""
|
||||
return self._connection_type
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""Return connected status."""
|
||||
return self._connected
|
||||
|
||||
@property
|
||||
def mac_address(self) -> str:
|
||||
"""Get MAC address."""
|
||||
return self._mac
|
||||
|
||||
@property
|
||||
def hostname(self) -> str:
|
||||
"""Get Name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def ip_address(self) -> str | None:
|
||||
"""Get IP address."""
|
||||
return self._ip_address
|
||||
|
||||
@property
|
||||
def last_activity(self) -> datetime | None:
|
||||
"""Return device last activity."""
|
||||
return self._last_activity
|
||||
|
||||
@property
|
||||
def ssid(self) -> str | None:
|
||||
"""Return device connected SSID."""
|
||||
return self._ssid
|
||||
|
||||
@property
|
||||
def wan_access(self) -> bool | None:
|
||||
"""Return device wan access."""
|
||||
return self._wan_access
|
||||
|
||||
|
||||
class SwitchInfo(TypedDict):
|
||||
"""FRITZ!Box switch info class."""
|
||||
|
||||
description: str
|
||||
friendly_name: str
|
||||
icon: str
|
||||
type: str
|
||||
callback_update: Callable
|
||||
callback_switch: Callable
|
||||
init_state: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConnectionInfo:
|
||||
"""Fritz sensor connection information class."""
|
||||
|
||||
connection: str
|
||||
mesh_role: MeshRoles
|
||||
wan_enabled: bool
|
||||
ipv6_active: bool
|
||||
|
||||
@@ -10,15 +10,10 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import (
|
||||
FRITZ_DATA_KEY,
|
||||
AvmWrapper,
|
||||
FritzConfigEntry,
|
||||
FritzData,
|
||||
FritzDevice,
|
||||
device_filter_out_from_trackers,
|
||||
)
|
||||
from .coordinator import FRITZ_DATA_KEY, AvmWrapper, FritzConfigEntry, FritzData
|
||||
from .entity import FritzDeviceBase
|
||||
from .helpers import device_filter_out_from_trackers
|
||||
from .models import FritzDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user