mirror of
https://github.com/home-assistant/core.git
synced 2025-09-22 03:19:33 +00:00
Compare commits
485 Commits
openai-mod
...
2025.8.3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3b1b095b47 | ||
![]() |
bb4f8adffe | ||
![]() |
61a50e77cf | ||
![]() |
82f94de0b8 | ||
![]() |
71b2d46afd | ||
![]() |
edc1989ff6 | ||
![]() |
2dad6fa298 | ||
![]() |
2f4e29ba71 | ||
![]() |
add75e06e3 | ||
![]() |
9194ddd4fe | ||
![]() |
2e7821d64a | ||
![]() |
bb9660269c | ||
![]() |
1bd5aa0ab0 | ||
![]() |
e4329ab8a5 | ||
![]() |
9414356a4d | ||
![]() |
0cd28e7fc1 | ||
![]() |
6383f9365c | ||
![]() |
cb8669c84f | ||
![]() |
d1698222f4 | ||
![]() |
a3f5c3f422 | ||
![]() |
945771098e | ||
![]() |
59d73138e7 | ||
![]() |
fe71b54c3e | ||
![]() |
7639e12ff2 | ||
![]() |
4b2a149072 | ||
![]() |
92b988a292 | ||
![]() |
1ca6c4b5b8 | ||
![]() |
81377be92f | ||
![]() |
38aba81f62 | ||
![]() |
27b32c5e93 | ||
![]() |
332996cc38 | ||
![]() |
199b7e8ba7 | ||
![]() |
122af46a92 | ||
![]() |
3dd091de44 | ||
![]() |
932c5ccf0f | ||
![]() |
4e52826664 | ||
![]() |
c30d778a54 | ||
![]() |
d7320f00ea | ||
![]() |
0bcc0f3fb9 | ||
![]() |
c551a133c1 | ||
![]() |
22e19e768e | ||
![]() |
0647222402 | ||
![]() |
83226ed015 | ||
![]() |
837472c12d | ||
![]() |
0b337c7e2a | ||
![]() |
1a0b61c98e | ||
![]() |
87a2d3e6d9 | ||
![]() |
2c1407f159 | ||
![]() |
1643d5df67 | ||
![]() |
5a49007b86 | ||
![]() |
312d8aaff5 | ||
![]() |
776726a053 | ||
![]() |
4213427b9c | ||
![]() |
82907e5b88 | ||
![]() |
56b4c554de | ||
![]() |
82390f6f7b | ||
![]() |
d9ebda4910 | ||
![]() |
8f94657b0c | ||
![]() |
b0ab3cddb8 | ||
![]() |
3d4d57fa32 | ||
![]() |
fed6f19edf | ||
![]() |
2725abf032 | ||
![]() |
bd1b81493c | ||
![]() |
c58a188179 | ||
![]() |
ffbb7a2ab4 | ||
![]() |
2ad470d172 | ||
![]() |
8b2fce9c33 | ||
![]() |
e22e7f1bcf | ||
![]() |
b5bd61b20a | ||
![]() |
391c9a679e | ||
![]() |
8d49cb1195 | ||
![]() |
dc8aaac6fb | ||
![]() |
1e87f0cab1 | ||
![]() |
1aeced0fe6 | ||
![]() |
7ed14f0afd | ||
![]() |
dc5d159ffb | ||
![]() |
5fdd04b860 | ||
![]() |
6f5d72fd81 | ||
![]() |
3158aa8891 | ||
![]() |
0c74e22069 | ||
![]() |
a88549315c | ||
![]() |
fde548b825 | ||
![]() |
91b10fb6d7 | ||
![]() |
39f41fe17d | ||
![]() |
3d39fb08e5 | ||
![]() |
a1731cd210 | ||
![]() |
0c31ec9bb6 | ||
![]() |
762c179b80 | ||
![]() |
66019953db | ||
![]() |
a2931efeeb | ||
![]() |
90c03f4115 | ||
![]() |
8afe3fed74 | ||
![]() |
3ef332e168 | ||
![]() |
8d821d9f98 | ||
![]() |
23619fb2d3 | ||
![]() |
bc70aeea85 | ||
![]() |
beca01e857 | ||
![]() |
4765d9da92 | ||
![]() |
7951e822be | ||
![]() |
c653bfff9f | ||
![]() |
2223bdb48e | ||
![]() |
42a3bef34a | ||
![]() |
6f4d405b26 | ||
![]() |
8edc5f0359 | ||
![]() |
efcffd1016 | ||
![]() |
ee32992010 | ||
![]() |
319128043e | ||
![]() |
f074d81c8b | ||
![]() |
d791d66104 | ||
![]() |
dd9bd50a7b | ||
![]() |
6243517271 | ||
![]() |
ad8ff7570d | ||
![]() |
c4c14bee36 | ||
![]() |
2cf5badc17 | ||
![]() |
0478f43b4b | ||
![]() |
d18f6273a8 | ||
![]() |
94bade0202 | ||
![]() |
855e8b08e9 | ||
![]() |
9820956b46 | ||
![]() |
1693299652 | ||
![]() |
75200a9426 | ||
![]() |
fa587cec38 | ||
![]() |
47946d0103 | ||
![]() |
d2586ca4ff | ||
![]() |
4e21ef5fbc | ||
![]() |
a9998b41a5 | ||
![]() |
0a72f31504 | ||
![]() |
f3a50c176d | ||
![]() |
b6b422775a | ||
![]() |
00baecd01e | ||
![]() |
b370b7a7f6 | ||
![]() |
baa2d751e4 | ||
![]() |
c8d54fcffc | ||
![]() |
80e3655bac | ||
![]() |
e5b0a366fe | ||
![]() |
20e78a15b4 | ||
![]() |
9d806aef88 | ||
![]() |
7e16973166 | ||
![]() |
e5f776fdc3 | ||
![]() |
83ccdb35f1 | ||
![]() |
52984f2fd1 | ||
![]() |
a548e13da5 | ||
![]() |
55301a50b2 | ||
![]() |
808273962d | ||
![]() |
094fe43557 | ||
![]() |
8f5bd51eef | ||
![]() |
faf0ded854 | ||
![]() |
d20302f97b | ||
![]() |
74c25496bc | ||
![]() |
67ecea0778 | ||
![]() |
164e5871cb | ||
![]() |
7a9966120e | ||
![]() |
d810b4ca38 | ||
![]() |
896062d669 | ||
![]() |
03bd133577 | ||
![]() |
4596c1644b | ||
![]() |
778fe96eb6 | ||
![]() |
a06557ed54 | ||
![]() |
641621d184 | ||
![]() |
b163f2b855 | ||
![]() |
0c0604e5bd | ||
![]() |
e0e4fc8afb | ||
![]() |
f832a2844f | ||
![]() |
4b0b268227 | ||
![]() |
dfc16d9f15 | ||
![]() |
4e3309bd22 | ||
![]() |
a5a45ce59f | ||
![]() |
6cb48da2f3 | ||
![]() |
ab5aac47b2 | ||
![]() |
d50b9405f0 | ||
![]() |
a2722f08c4 | ||
![]() |
aa700c3982 | ||
![]() |
3b1bb41129 | ||
![]() |
79ef51fb07 | ||
![]() |
53769da55e | ||
![]() |
82d153a240 | ||
![]() |
0dac635478 | ||
![]() |
90fc7d314b | ||
![]() |
636c1b7e4f | ||
![]() |
49c23de2d2 | ||
![]() |
e48820b2c1 | ||
![]() |
2b7a434677 | ||
![]() |
9ef7c6c99a | ||
![]() |
b789c11217 | ||
![]() |
5e8cd19cc3 | ||
![]() |
027052440d | ||
![]() |
47a7ed4084 | ||
![]() |
89f6cfeb81 | ||
![]() |
c268e57ba7 | ||
![]() |
138c19126b | ||
![]() |
c459ceba73 | ||
![]() |
8d0ceff652 | ||
![]() |
1d383e80a4 | ||
![]() |
6a17a12be5 | ||
![]() |
3a8d962d34 | ||
![]() |
7e5cf17cf4 | ||
![]() |
214940d04f | ||
![]() |
6877fdaf5b | ||
![]() |
35d0c254a2 | ||
![]() |
9649fbc189 | ||
![]() |
b60b1fc0c6 | ||
![]() |
6b93f6d75c | ||
![]() |
c8069a383e | ||
![]() |
6857e87b30 | ||
![]() |
a095631f4f | ||
![]() |
c59fbdeec1 | ||
![]() |
b521b1e64c | ||
![]() |
073589ae19 | ||
![]() |
9435b0ad3a | ||
![]() |
1662d36125 | ||
![]() |
70e54fdadd | ||
![]() |
38d0ebb8ba | ||
![]() |
15cb48badb | ||
![]() |
22214e8d31 | ||
![]() |
fc04e0b2cc | ||
![]() |
3fc6ebdb43 | ||
![]() |
3ccb7deb3c | ||
![]() |
f5f63b914a | ||
![]() |
bd0a3f5a5d | ||
![]() |
ab9eebd092 | ||
![]() |
68c43099d9 | ||
![]() |
041c417164 | ||
![]() |
537d09c697 | ||
![]() |
21e3b8da92 | ||
![]() |
d390681360 | ||
![]() |
918ec78348 | ||
![]() |
1deae3ee1a | ||
![]() |
59eace67df | ||
![]() |
7eb7c66e3f | ||
![]() |
aa2941592d | ||
![]() |
29daf136d2 | ||
![]() |
3da3cf7f52 | ||
![]() |
d8c93d54d5 | ||
![]() |
0799ee9fba | ||
![]() |
9d31403984 | ||
![]() |
02f87cba9b | ||
![]() |
8193259e02 | ||
![]() |
6306baa3c9 | ||
![]() |
d481a694f1 | ||
![]() |
5b54784378 | ||
![]() |
edca3fc0b7 | ||
![]() |
daea76c2f1 | ||
![]() |
160b61e0b9 | ||
![]() |
fc900a632a | ||
![]() |
1b58809655 | ||
![]() |
223c34056d | ||
![]() |
99ee56a4dd | ||
![]() |
91be25a292 | ||
![]() |
a21af78aa1 | ||
![]() |
70cfdfa231 | ||
![]() |
a5b075af68 | ||
![]() |
c4d4ef884e | ||
![]() |
ba4e7e50e0 | ||
![]() |
dd0b23afb0 | ||
![]() |
779f0afcc4 | ||
![]() |
d8016f7f41 | ||
![]() |
25169e9075 | ||
![]() |
260ca70785 | ||
![]() |
69e3a5bc34 | ||
![]() |
1a75a88c76 | ||
![]() |
6c2a662838 | ||
![]() |
749fc318ca | ||
![]() |
828f979c78 | ||
![]() |
1eb6d5fe32 | ||
![]() |
5930ac6425 | ||
![]() |
15e45df8a7 | ||
![]() |
a79d2da9a3 | ||
![]() |
ac86f2e2ba | ||
![]() |
03ee97d38f | ||
![]() |
06233b5134 | ||
![]() |
9d66b19c03 | ||
![]() |
bb6bcfdd01 | ||
![]() |
8e9e304608 | ||
![]() |
6b641411a0 | ||
![]() |
6f8214bbb4 | ||
![]() |
f66e83f33e | ||
![]() |
2ee82e1d6f | ||
![]() |
0dd1e0cabb | ||
![]() |
45ae34cc0e | ||
![]() |
73e578b168 | ||
![]() |
52ee5d53ee | ||
![]() |
62713b1371 | ||
![]() |
c4c4463c63 | ||
![]() |
7e2fd6e47b | ||
![]() |
9f45801409 | ||
![]() |
aaec243bf4 | ||
![]() |
b67e85e8da | ||
![]() |
25407c0f4b | ||
![]() |
09e7d8d1a5 | ||
![]() |
ff7c125334 | ||
![]() |
3d6f868cbc | ||
![]() |
378c3af9df | ||
![]() |
c7271d1af9 | ||
![]() |
87400c6a17 | ||
![]() |
692a1119a6 | ||
![]() |
2e728eb7de | ||
![]() |
45ec9c7dad | ||
![]() |
62ee1fbc64 | ||
![]() |
3c1aa9d9de | ||
![]() |
bf568b22d7 | ||
![]() |
596f6cd216 | ||
![]() |
cf05f1046d | ||
![]() |
7f9be420d2 | ||
![]() |
dda46e7e0b | ||
![]() |
b1dd742a57 | ||
![]() |
5af4290b77 | ||
![]() |
8339516fb4 | ||
![]() |
aa1314c1d5 | ||
![]() |
92ad922ddc | ||
![]() |
e518e7beac | ||
![]() |
483d814a8f | ||
![]() |
8f795f021c | ||
![]() |
d823b574c0 | ||
![]() |
49bd15718c | ||
![]() |
d3f18c1678 | ||
![]() |
5ef17c8588 | ||
![]() |
e8b8d31027 | ||
![]() |
978ee3870c | ||
![]() |
b3862591ea | ||
![]() |
1895db0ddd | ||
![]() |
ee2cf961f6 | ||
![]() |
9a364ec729 | ||
![]() |
96529ec245 | ||
![]() |
8fc8220924 | ||
![]() |
386f709fd3 | ||
![]() |
d088fccb88 | ||
![]() |
2a5448835f | ||
![]() |
a71eecaaa4 | ||
![]() |
46d810b9f9 | ||
![]() |
48c4240a5d | ||
![]() |
bf05c23414 | ||
![]() |
db1e6a0d98 | ||
![]() |
4ad35e8421 | ||
![]() |
850e04d9aa | ||
![]() |
95c5a91f01 | ||
![]() |
140f56aeaa | ||
![]() |
40ce228c9c | ||
![]() |
18c5437fe7 | ||
![]() |
ebad1ff4cc | ||
![]() |
a68e722c92 | ||
![]() |
05935bbc01 | ||
![]() |
c67636b4f6 | ||
![]() |
777b3128bb | ||
![]() |
ab6cd0eb41 | ||
![]() |
f35558413a | ||
![]() |
e30d405625 | ||
![]() |
622cce03a1 | ||
![]() |
1fa9141ce1 | ||
![]() |
a060f7486f | ||
![]() |
dbb5730389 | ||
![]() |
431b2aa1d5 | ||
![]() |
c99d81a554 | ||
![]() |
ff4dc393cf | ||
![]() |
d384bee576 | ||
![]() |
f0cb5d5480 | ||
![]() |
725799c73e | ||
![]() |
dc6d2e3e84 | ||
![]() |
4a7d06a68a | ||
![]() |
cd800da357 | ||
![]() |
4c8ab8eb64 | ||
![]() |
60f4d29d60 | ||
![]() |
68b7d09476 | ||
![]() |
c3eb6dea11 | ||
![]() |
f428ffde87 | ||
![]() |
fa207860a0 | ||
![]() |
959c3a8a99 | ||
![]() |
254ccca4e5 | ||
![]() |
5b08724d81 | ||
![]() |
ea2b3b3ff3 | ||
![]() |
a33760bc1a | ||
![]() |
4ea7ad52b1 | ||
![]() |
dac75d1902 | ||
![]() |
0e9ced3c00 | ||
![]() |
22d0fbcbd2 | ||
![]() |
57b641b97d | ||
![]() |
27bd6d2e38 | ||
![]() |
427e5d81df | ||
![]() |
b6bd92ed19 | ||
![]() |
7976729e76 | ||
![]() |
5aa0d0dc81 | ||
![]() |
e1501d7510 | ||
![]() |
be5109fddf | ||
![]() |
c5cf9b07b7 | ||
![]() |
002b7c6789 | ||
![]() |
e017dc80a0 | ||
![]() |
aab7381553 | ||
![]() |
cbf4409db3 | ||
![]() |
56fb59e48e | ||
![]() |
971bd56bee | ||
![]() |
b2710c1bce | ||
![]() |
a069b59efc | ||
![]() |
02eb1dd533 | ||
![]() |
b3130c7929 | ||
![]() |
aad1dbecb4 | ||
![]() |
65109ea000 | ||
![]() |
356ac74fa5 | ||
![]() |
f3513f7f29 | ||
![]() |
4bbb94f43d | ||
![]() |
c1fa721a57 | ||
![]() |
e3ffb41650 | ||
![]() |
123cce6d96 | ||
![]() |
6920dec352 | ||
![]() |
f7cc260336 | ||
![]() |
b7da31a021 | ||
![]() |
95d4dc678c | ||
![]() |
7e9da052ca | ||
![]() |
59ece455d9 | ||
![]() |
3ba144c8b2 | ||
![]() |
456f992b7e | ||
![]() |
0675e34c62 | ||
![]() |
190c98f5a8 | ||
![]() |
c6bb26be89 | ||
![]() |
d57c5ffa8f | ||
![]() |
68889e1790 | ||
![]() |
8fdc50a29f | ||
![]() |
5656b4c20d | ||
![]() |
b6edcc9422 | ||
![]() |
7a3eb53453 | ||
![]() |
11a2c73e8a | ||
![]() |
1644484c92 | ||
![]() |
8e0a89dc2f | ||
![]() |
9e4b8df344 | ||
![]() |
69fdc1d269 | ||
![]() |
56e0aa103d | ||
![]() |
caf0492009 | ||
![]() |
c6d0aad3d3 | ||
![]() |
1f59b735c6 | ||
![]() |
87af9fc8ba | ||
![]() |
691a0ca065 | ||
![]() |
80384b89a5 | ||
![]() |
f7672985ed | ||
![]() |
d4374dbcc7 | ||
![]() |
c4ddcd64c8 | ||
![]() |
c802430066 | ||
![]() |
649fbfc729 | ||
![]() |
80c52ad8ea | ||
![]() |
150d4716fa | ||
![]() |
dc2736580f | ||
![]() |
f1272ef513 | ||
![]() |
3c2fa023b4 | ||
![]() |
5cf5be8c9c | ||
![]() |
63b21fda1a | ||
![]() |
d87379d083 | ||
![]() |
0990cef917 | ||
![]() |
962ad99c20 | ||
![]() |
9c9836defd | ||
![]() |
e951fc401c | ||
![]() |
00e2a177a5 | ||
![]() |
b6d316c8f2 | ||
![]() |
b8425de0d0 | ||
![]() |
d51a44acbc | ||
![]() |
435465e569 | ||
![]() |
3b047859f9 | ||
![]() |
91cdf1a367 | ||
![]() |
2377b136f3 | ||
![]() |
186c4e7038 | ||
![]() |
d303a7d17e | ||
![]() |
14f059c766 | ||
![]() |
4a10370932 | ||
![]() |
672ffa5984 | ||
![]() |
3d3f2527cb | ||
![]() |
5c3b279f95 | ||
![]() |
59bcf1167a | ||
![]() |
b4d789f8e2 | ||
![]() |
f4ca56052b | ||
![]() |
74f9549431 | ||
![]() |
9650727515 | ||
![]() |
c965da6559 | ||
![]() |
9077965214 | ||
![]() |
2b7992e849 | ||
![]() |
5d6b02f470 | ||
![]() |
a274961593 | ||
![]() |
4e163c4591 | ||
![]() |
3ffec2a655 | ||
![]() |
c646658643 | ||
![]() |
342b4c3442 | ||
![]() |
eb58c10e5e | ||
![]() |
f42e7d982f | ||
![]() |
898ef43750 | ||
![]() |
f806e6ba49 | ||
![]() |
c23bfb1b39 | ||
![]() |
a2ffe32b02 | ||
![]() |
0f32b6331d | ||
![]() |
9a4959560e | ||
![]() |
41ab7b346c |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 4
|
||||
CACHE_VERSION: 6
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.8"
|
||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.29.4
|
||||
uses: github/codeql-action/init@v3.29.5
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.29.4
|
||||
uses: github/codeql-action/analyze@v3.29.5
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
@@ -53,6 +53,7 @@ homeassistant.components.air_quality.*
|
||||
homeassistant.components.airgradient.*
|
||||
homeassistant.components.airly.*
|
||||
homeassistant.components.airnow.*
|
||||
homeassistant.components.airos.*
|
||||
homeassistant.components.airq.*
|
||||
homeassistant.components.airthings.*
|
||||
homeassistant.components.airthings_ble.*
|
||||
@@ -501,6 +502,7 @@ homeassistant.components.tag.*
|
||||
homeassistant.components.tailscale.*
|
||||
homeassistant.components.tailwind.*
|
||||
homeassistant.components.tami4.*
|
||||
homeassistant.components.tankerkoenig.*
|
||||
homeassistant.components.tautulli.*
|
||||
homeassistant.components.tcp.*
|
||||
homeassistant.components.technove.*
|
||||
@@ -546,6 +548,7 @@ homeassistant.components.valve.*
|
||||
homeassistant.components.velbus.*
|
||||
homeassistant.components.vlc_telnet.*
|
||||
homeassistant.components.vodafone_station.*
|
||||
homeassistant.components.volvo.*
|
||||
homeassistant.components.wake_on_lan.*
|
||||
homeassistant.components.wake_word.*
|
||||
homeassistant.components.wallbox.*
|
||||
|
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -67,6 +67,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/airly/ @bieniu
|
||||
/homeassistant/components/airnow/ @asymworks
|
||||
/tests/components/airnow/ @asymworks
|
||||
/homeassistant/components/airos/ @CoMPaTech
|
||||
/tests/components/airos/ @CoMPaTech
|
||||
/homeassistant/components/airq/ @Sibgatulin @dl2080
|
||||
/tests/components/airq/ @Sibgatulin @dl2080
|
||||
/homeassistant/components/airthings/ @danielhiversen @LaStrada
|
||||
@@ -1706,6 +1708,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/voip/ @balloob @synesthesiam @jaminh
|
||||
/homeassistant/components/volumio/ @OnFreund
|
||||
/tests/components/volumio/ @OnFreund
|
||||
/homeassistant/components/volvo/ @thomasddn
|
||||
/tests/components/volvo/ @thomasddn
|
||||
/homeassistant/components/volvooncall/ @molobrakos
|
||||
/tests/components/volvooncall/ @molobrakos
|
||||
/homeassistant/components/vulcan/ @Antoni-Czaplicki
|
||||
|
2
Dockerfile
generated
2
Dockerfile
generated
@@ -31,7 +31,7 @@ RUN \
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.7.1
|
||||
RUN pip3 install uv==0.8.9
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
5
homeassistant/brands/frient.json
Normal file
5
homeassistant/brands/frient.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "frient",
|
||||
"name": "Frient",
|
||||
"iot_standards": ["zigbee"]
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "third_reality",
|
||||
"name": "Third Reality",
|
||||
"iot_standards": ["zigbee"]
|
||||
"iot_standards": ["matter", "zigbee"]
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "ubiquiti",
|
||||
"name": "Ubiquiti",
|
||||
"integrations": ["unifi", "unifi_direct", "unifiled", "unifiprotect"]
|
||||
"integrations": ["airos", "unifi", "unifi_direct", "unifiled", "unifiprotect"]
|
||||
}
|
||||
|
42
homeassistant/components/airos/__init__.py
Normal file
42
homeassistant/components/airos/__init__.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""The Ubiquiti airOS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from airos.airos8 import AirOS
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||
|
||||
_PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||
"""Set up Ubiquiti airOS from a config entry."""
|
||||
|
||||
# By default airOS 8 comes with self-signed SSL certificates,
|
||||
# with no option in the web UI to change or upload a custom certificate.
|
||||
session = async_get_clientsession(hass, verify_ssl=False)
|
||||
|
||||
airos_device = AirOS(
|
||||
host=entry.data[CONF_HOST],
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
session=session,
|
||||
)
|
||||
|
||||
coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device)
|
||||
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: AirOSConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
82
homeassistant/components/airos/config_flow.py
Normal file
82
homeassistant/components/airos/config_flow.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Config flow for the Ubiquiti airOS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from airos.exceptions import (
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
AirOSKeyDataMissingError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirOS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_USERNAME, default="ubnt"): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Ubiquiti airOS."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
# By default airOS 8 comes with self-signed SSL certificates,
|
||||
# with no option in the web UI to change or upload a custom certificate.
|
||||
session = async_get_clientsession(self.hass, verify_ssl=False)
|
||||
|
||||
airos_device = AirOS(
|
||||
host=user_input[CONF_HOST],
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
session=session,
|
||||
)
|
||||
try:
|
||||
await airos_device.login()
|
||||
airos_data = await airos_device.status()
|
||||
|
||||
except (
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
):
|
||||
errors["base"] = "cannot_connect"
|
||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||
errors["base"] = "invalid_auth"
|
||||
except AirOSKeyDataMissingError:
|
||||
errors["base"] = "key_data_missing"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(airos_data.derived.mac)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=airos_data.host.hostname, data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
9
homeassistant/components/airos/const.py
Normal file
9
homeassistant/components/airos/const.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Constants for the Ubiquiti airOS integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
DOMAIN = "airos"
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
MANUFACTURER = "Ubiquiti"
|
70
homeassistant/components/airos/coordinator.py
Normal file
70
homeassistant/components/airos/coordinator.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""DataUpdateCoordinator for AirOS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from airos.airos8 import AirOS, AirOSData
|
||||
from airos.exceptions import (
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type AirOSConfigEntry = ConfigEntry[AirOSDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
||||
"""Class to manage fetching AirOS data from single endpoint."""
|
||||
|
||||
config_entry: AirOSConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: AirOSConfigEntry, airos_device: AirOS
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.airos_device = airos_device
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> AirOSData:
|
||||
"""Fetch data from AirOS."""
|
||||
try:
|
||||
await self.airos_device.login()
|
||||
return await self.airos_device.status()
|
||||
except (AirOSConnectionAuthenticationError,) as err:
|
||||
_LOGGER.exception("Error authenticating with airOS device")
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from err
|
||||
except (
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
TimeoutError,
|
||||
) as err:
|
||||
_LOGGER.error("Error connecting to airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
except (AirOSDataMissingError,) as err:
|
||||
_LOGGER.error("Expected data not returned by airOS device: %s", err)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="error_data_missing",
|
||||
) from err
|
33
homeassistant/components/airos/diagnostics.py
Normal file
33
homeassistant/components/airos/diagnostics.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Diagnostics support for airOS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AirOSConfigEntry
|
||||
|
||||
IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related
|
||||
HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address
|
||||
TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD]
|
||||
TO_REDACT_AIROS = [
|
||||
"hostname", # Prevent leaking device naming
|
||||
"essid", # Network SSID
|
||||
"lat", # GPS latitude to prevent exposing location data.
|
||||
"lon", # GPS longitude to prevent exposing location data.
|
||||
*HW_REDACT,
|
||||
*IP_REDACT,
|
||||
]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AirOSConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {
|
||||
"entry_data": async_redact_data(entry.data, TO_REDACT_HA),
|
||||
"data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS),
|
||||
}
|
36
homeassistant/components/airos/entity.py
Normal file
36
homeassistant/components/airos/entity.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Generic AirOS Entity Class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import AirOSDataUpdateCoordinator
|
||||
|
||||
|
||||
class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]):
|
||||
"""Represent a AirOS Entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: AirOSDataUpdateCoordinator) -> None:
|
||||
"""Initialise the gateway."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
airos_data = self.coordinator.data
|
||||
|
||||
configuration_url: str | None = (
|
||||
f"https://{coordinator.config_entry.data[CONF_HOST]}"
|
||||
)
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, airos_data.derived.mac)},
|
||||
configuration_url=configuration_url,
|
||||
identifiers={(DOMAIN, str(airos_data.host.device_id))},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=airos_data.host.devmodel,
|
||||
name=airos_data.host.hostname,
|
||||
sw_version=airos_data.host.fwversion,
|
||||
)
|
10
homeassistant/components/airos/manifest.json
Normal file
10
homeassistant/components/airos/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "airos",
|
||||
"name": "Ubiquiti airOS",
|
||||
"codeowners": ["@CoMPaTech"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["airos==0.2.11"]
|
||||
}
|
72
homeassistant/components/airos/quality_scale.yaml
Normal file
72
homeassistant/components/airos/quality_scale.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: airOS does not have 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: airOS does not have actions
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: local_polling without 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: airOS does not have actions
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: done
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: todo
|
||||
comment: prepared binary_sensors will provide this
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: no (custom) icons used or envisioned
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
145
homeassistant/components/airos/sensor.py
Normal file
145
homeassistant/components/airos/sensor.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""AirOS Sensor component for Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from airos.data import NetRole, WirelessMode
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
UnitOfDataRate,
|
||||
UnitOfFrequency,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator
|
||||
from .entity import AirOSEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
WIRELESS_MODE_OPTIONS = [mode.value.replace("-", "_").lower() for mode in WirelessMode]
|
||||
NETROLE_OPTIONS = [mode.value for mode in NetRole]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AirOSSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describe an AirOS sensor."""
|
||||
|
||||
value_fn: Callable[[AirOSData], StateType]
|
||||
|
||||
|
||||
SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||
AirOSSensorEntityDescription(
|
||||
key="host_cpuload",
|
||||
translation_key="host_cpuload",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.host.cpuload,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="host_netrole",
|
||||
translation_key="host_netrole",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda data: data.host.netrole.value,
|
||||
options=NETROLE_OPTIONS,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_frequency",
|
||||
translation_key="wireless_frequency",
|
||||
native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.wireless.frequency,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_essid",
|
||||
translation_key="wireless_essid",
|
||||
value_fn=lambda data: data.wireless.essid,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_antenna_gain",
|
||||
translation_key="wireless_antenna_gain",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.wireless.antenna_gain,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_throughput_tx",
|
||||
translation_key="wireless_throughput_tx",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.wireless.throughput.tx,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_throughput_rx",
|
||||
translation_key="wireless_throughput_rx",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.wireless.throughput.rx,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_polling_dl_capacity",
|
||||
translation_key="wireless_polling_dl_capacity",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.wireless.polling.dl_capacity,
|
||||
),
|
||||
AirOSSensorEntityDescription(
|
||||
key="wireless_polling_ul_capacity",
|
||||
translation_key="wireless_polling_ul_capacity",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.wireless.polling.ul_capacity,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AirOSConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the AirOS sensors from a config entry."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(AirOSSensor(coordinator, description) for description in SENSORS)
|
||||
|
||||
|
||||
class AirOSSensor(AirOSEntity, SensorEntity):
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
entity_description: AirOSSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirOSDataUpdateCoordinator,
|
||||
description: AirOSSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.data.derived.mac}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
80
homeassistant/components/airos/strings.json
Normal file
80
homeassistant/components/airos/strings.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Ubiquiti airOS device",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "IP address or hostname of the airOS device",
|
||||
"username": "Administrator username for the airOS device, normally 'ubnt'",
|
||||
"password": "Password configured through the UISP app or web interface"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"key_data_missing": "Expected data not returned from the device, check the documentation for supported devices",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"host_cpuload": {
|
||||
"name": "CPU load"
|
||||
},
|
||||
"host_netrole": {
|
||||
"name": "Network role",
|
||||
"state": {
|
||||
"bridge": "Bridge",
|
||||
"router": "Router"
|
||||
}
|
||||
},
|
||||
"wireless_frequency": {
|
||||
"name": "Wireless frequency"
|
||||
},
|
||||
"wireless_essid": {
|
||||
"name": "Wireless SSID"
|
||||
},
|
||||
"wireless_antenna_gain": {
|
||||
"name": "Antenna gain"
|
||||
},
|
||||
"wireless_throughput_tx": {
|
||||
"name": "Throughput transmit (actual)"
|
||||
},
|
||||
"wireless_throughput_rx": {
|
||||
"name": "Throughput receive (actual)"
|
||||
},
|
||||
"wireless_polling_dl_capacity": {
|
||||
"name": "Download capacity"
|
||||
},
|
||||
"wireless_polling_ul_capacity": {
|
||||
"name": "Upload capacity"
|
||||
},
|
||||
"wireless_remote_hostname": {
|
||||
"name": "Remote hostname"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_auth": {
|
||||
"message": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"cannot_connect": {
|
||||
"message": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"key_data_missing": {
|
||||
"message": "Key data not returned from device"
|
||||
},
|
||||
"error_data_missing": {
|
||||
"message": "Data incomplete or missing"
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,21 +7,18 @@ import logging
|
||||
|
||||
from airthings import Airthings
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_SECRET
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
|
||||
"""Set up Airthings from a config entry."""
|
||||
@@ -31,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
@@ -5,6 +5,7 @@ import logging
|
||||
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -13,15 +14,23 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
|
||||
"""Coordinator for Airthings data updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
airthings: Airthings,
|
||||
config_entry: AirthingsConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_method=self._update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.7.0"]
|
||||
"requirements": ["aioairzone-cloud==0.7.1"]
|
||||
}
|
||||
|
@@ -1,9 +1,13 @@
|
||||
"""Alexa Devices integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import CONF_COUNTRY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import _LOGGER, COUNTRY_DOMAINS, DOMAIN
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -12,11 +16,20 @@ PLATFORMS = [
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Alexa Devices component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Set up Alexa Devices platform."""
|
||||
|
||||
coordinator = AmazonDevicesCoordinator(hass, entry)
|
||||
session = aiohttp_client.async_create_clientsession(hass)
|
||||
coordinator = AmazonDevicesCoordinator(hass, entry, session)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -27,10 +40,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
if entry.version == 1 and entry.minor_version == 0:
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s.%s", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
# Convert country in domain
|
||||
country = entry.data[CONF_COUNTRY]
|
||||
domain = COUNTRY_DOMAINS.get(country, country)
|
||||
|
||||
# Save domain and remove country
|
||||
new_data = entry.data.copy()
|
||||
new_data.update({"site": f"https://www.amazon.{domain}"})
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data=new_data, version=1, minor_version=1
|
||||
)
|
||||
|
||||
_LOGGER.info(
|
||||
"Migration to version %s.%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
await coordinator.api.close()
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
@@ -10,15 +10,14 @@ from aioamazondevices.exceptions import (
|
||||
CannotAuthenticate,
|
||||
CannotConnect,
|
||||
CannotRetrieveData,
|
||||
WrongCountry,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.selector import CountrySelector
|
||||
|
||||
from .const import CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
@@ -33,23 +32,22 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
session = aiohttp_client.async_create_clientsession(hass)
|
||||
api = AmazonEchoApi(
|
||||
data[CONF_COUNTRY],
|
||||
session,
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
data = await api.login_mode_interactive(data[CONF_CODE])
|
||||
finally:
|
||||
await api.close()
|
||||
|
||||
return data
|
||||
return await api.login_mode_interactive(data[CONF_CODE])
|
||||
|
||||
|
||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Alexa Devices."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -64,8 +62,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "invalid_auth"
|
||||
except CannotRetrieveData:
|
||||
errors["base"] = "cannot_retrieve_data"
|
||||
except WrongCountry:
|
||||
errors["base"] = "wrong_country"
|
||||
else:
|
||||
await self.async_set_unique_id(data["customer_info"]["user_id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
@@ -80,9 +76,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_COUNTRY, default=self.hass.config.country
|
||||
): CountrySelector(),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
|
@@ -6,3 +6,22 @@ _LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "alexa_devices"
|
||||
CONF_LOGIN_DATA = "login_data"
|
||||
|
||||
DEFAULT_DOMAIN = {"domain": "com"}
|
||||
COUNTRY_DOMAINS = {
|
||||
"ar": DEFAULT_DOMAIN,
|
||||
"at": DEFAULT_DOMAIN,
|
||||
"au": {"domain": "com.au"},
|
||||
"be": {"domain": "com.be"},
|
||||
"br": DEFAULT_DOMAIN,
|
||||
"gb": {"domain": "co.uk"},
|
||||
"il": DEFAULT_DOMAIN,
|
||||
"jp": {"domain": "co.jp"},
|
||||
"mx": {"domain": "com.mx"},
|
||||
"no": DEFAULT_DOMAIN,
|
||||
"nz": {"domain": "com.au"},
|
||||
"pl": DEFAULT_DOMAIN,
|
||||
"tr": {"domain": "com.tr"},
|
||||
"us": DEFAULT_DOMAIN,
|
||||
"za": {"domain": "co.za"},
|
||||
}
|
||||
|
@@ -8,9 +8,10 @@ from aioamazondevices.exceptions import (
|
||||
CannotConnect,
|
||||
CannotRetrieveData,
|
||||
)
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@@ -31,6 +32,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
session: ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
super().__init__(
|
||||
@@ -41,7 +43,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
||||
)
|
||||
self.api = AmazonEchoApi(
|
||||
entry.data[CONF_COUNTRY],
|
||||
session,
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_LOGIN_DATA],
|
||||
|
@@ -38,5 +38,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_sound": {
|
||||
"service": "mdi:cast-audio"
|
||||
},
|
||||
"send_text_command": {
|
||||
"service": "mdi:microphone-message"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioamazondevices==3.5.1"]
|
||||
"requirements": ["aioamazondevices==5.0.0"]
|
||||
}
|
||||
|
@@ -48,17 +48,17 @@ rules:
|
||||
comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: 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
|
||||
repair-issues:
|
||||
@@ -70,5 +70,5 @@ rules:
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
|
121
homeassistant/components/alexa_devices/services.py
Normal file
121
homeassistant/components/alexa_devices/services.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Support for services."""
|
||||
|
||||
from aioamazondevices.sounds import SOUNDS_LIST
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AmazonConfigEntry
|
||||
|
||||
ATTR_TEXT_COMMAND = "text_command"
|
||||
ATTR_SOUND = "sound"
|
||||
ATTR_SOUND_VARIANT = "sound_variant"
|
||||
SERVICE_TEXT_COMMAND = "send_text_command"
|
||||
SERVICE_SOUND_NOTIFICATION = "send_sound"
|
||||
|
||||
SCHEMA_SOUND_SERVICE = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_SOUND): cv.string,
|
||||
vol.Required(ATTR_SOUND_VARIANT): cv.positive_int,
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
},
|
||||
)
|
||||
SCHEMA_CUSTOM_COMMAND = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_TEXT_COMMAND): cv.string,
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_entry_id_for_service_call(
|
||||
call: ServiceCall,
|
||||
) -> tuple[dr.DeviceEntry, AmazonConfigEntry]:
|
||||
"""Get the entry ID related to a service call (by device ID)."""
|
||||
device_registry = dr.async_get(call.hass)
|
||||
device_id = call.data[ATTR_DEVICE_ID]
|
||||
if (device_entry := device_registry.async_get(device_id)) is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_device_id",
|
||||
translation_placeholders={"device_id": device_id},
|
||||
)
|
||||
|
||||
for entry_id in device_entry.config_entries:
|
||||
if (entry := call.hass.config_entries.async_get_entry(entry_id)) is None:
|
||||
continue
|
||||
if entry.domain == DOMAIN:
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_loaded",
|
||||
translation_placeholders={"entry": entry.title},
|
||||
)
|
||||
return (device_entry, entry)
|
||||
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="config_entry_not_found",
|
||||
translation_placeholders={"device_id": device_id},
|
||||
)
|
||||
|
||||
|
||||
async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
|
||||
"""Execute action on the device."""
|
||||
device, config_entry = async_get_entry_id_for_service_call(call)
|
||||
assert device.serial_number
|
||||
value: str = call.data[attribute]
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
if attribute == ATTR_SOUND:
|
||||
variant: int = call.data[ATTR_SOUND_VARIANT]
|
||||
pad = "_" if variant > 10 else "_0"
|
||||
file = f"{value}{pad}{variant!s}"
|
||||
if value not in SOUNDS_LIST or variant > SOUNDS_LIST[value]:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_sound_value",
|
||||
translation_placeholders={"sound": value, "variant": str(variant)},
|
||||
)
|
||||
await coordinator.api.call_alexa_sound(
|
||||
coordinator.data[device.serial_number], file
|
||||
)
|
||||
elif attribute == ATTR_TEXT_COMMAND:
|
||||
await coordinator.api.call_alexa_text_command(
|
||||
coordinator.data[device.serial_number], value
|
||||
)
|
||||
|
||||
|
||||
async def async_send_sound_notification(call: ServiceCall) -> None:
|
||||
"""Send a sound notification to a AmazonDevice."""
|
||||
await _async_execute_action(call, ATTR_SOUND)
|
||||
|
||||
|
||||
async def async_send_text_command(call: ServiceCall) -> None:
|
||||
"""Send a custom command to a AmazonDevice."""
|
||||
await _async_execute_action(call, ATTR_TEXT_COMMAND)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Amazon Devices integration."""
|
||||
for service_name, method, schema in (
|
||||
(
|
||||
SERVICE_SOUND_NOTIFICATION,
|
||||
async_send_sound_notification,
|
||||
SCHEMA_SOUND_SERVICE,
|
||||
),
|
||||
(
|
||||
SERVICE_TEXT_COMMAND,
|
||||
async_send_text_command,
|
||||
SCHEMA_CUSTOM_COMMAND,
|
||||
),
|
||||
):
|
||||
hass.services.async_register(DOMAIN, service_name, method, schema=schema)
|
504
homeassistant/components/alexa_devices/services.yaml
Normal file
504
homeassistant/components/alexa_devices/services.yaml
Normal file
@@ -0,0 +1,504 @@
|
||||
send_text_command:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: alexa_devices
|
||||
text_command:
|
||||
required: true
|
||||
example: "Play B.B.C. on TuneIn"
|
||||
selector:
|
||||
text:
|
||||
|
||||
send_sound:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: alexa_devices
|
||||
sound_variant:
|
||||
required: true
|
||||
example: 1
|
||||
default: 1
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 50
|
||||
sound:
|
||||
required: true
|
||||
example: amzn_sfx_doorbell_chime
|
||||
default: amzn_sfx_doorbell_chime
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- air_horn
|
||||
- air_horns
|
||||
- airboat
|
||||
- airport
|
||||
- aliens
|
||||
- amzn_sfx_airplane_takeoff_whoosh
|
||||
- amzn_sfx_army_march_clank_7x
|
||||
- amzn_sfx_army_march_large_8x
|
||||
- amzn_sfx_army_march_small_8x
|
||||
- amzn_sfx_baby_big_cry
|
||||
- amzn_sfx_baby_cry
|
||||
- amzn_sfx_baby_fuss
|
||||
- amzn_sfx_battle_group_clanks
|
||||
- amzn_sfx_battle_man_grunts
|
||||
- amzn_sfx_battle_men_grunts
|
||||
- amzn_sfx_battle_men_horses
|
||||
- amzn_sfx_battle_noisy_clanks
|
||||
- amzn_sfx_battle_yells_men
|
||||
- amzn_sfx_battle_yells_men_run
|
||||
- amzn_sfx_bear_groan_roar
|
||||
- amzn_sfx_bear_roar_grumble
|
||||
- amzn_sfx_bear_roar_small
|
||||
- amzn_sfx_beep_1x
|
||||
- amzn_sfx_bell_med_chime
|
||||
- amzn_sfx_bell_short_chime
|
||||
- amzn_sfx_bell_timer
|
||||
- amzn_sfx_bicycle_bell_ring
|
||||
- amzn_sfx_bird_chickadee_chirp_1x
|
||||
- amzn_sfx_bird_chickadee_chirps
|
||||
- amzn_sfx_bird_forest
|
||||
- amzn_sfx_bird_forest_short
|
||||
- amzn_sfx_bird_robin_chirp_1x
|
||||
- amzn_sfx_boing_long_1x
|
||||
- amzn_sfx_boing_med_1x
|
||||
- amzn_sfx_boing_short_1x
|
||||
- amzn_sfx_bus_drive_past
|
||||
- amzn_sfx_buzz_electronic
|
||||
- amzn_sfx_buzzer_loud_alarm
|
||||
- amzn_sfx_buzzer_small
|
||||
- amzn_sfx_car_accelerate
|
||||
- amzn_sfx_car_accelerate_noisy
|
||||
- amzn_sfx_car_click_seatbelt
|
||||
- amzn_sfx_car_close_door_1x
|
||||
- amzn_sfx_car_drive_past
|
||||
- amzn_sfx_car_honk_1x
|
||||
- amzn_sfx_car_honk_2x
|
||||
- amzn_sfx_car_honk_3x
|
||||
- amzn_sfx_car_honk_long_1x
|
||||
- amzn_sfx_car_into_driveway
|
||||
- amzn_sfx_car_into_driveway_fast
|
||||
- amzn_sfx_car_slam_door_1x
|
||||
- amzn_sfx_car_undo_seatbelt
|
||||
- amzn_sfx_cat_angry_meow_1x
|
||||
- amzn_sfx_cat_angry_screech_1x
|
||||
- amzn_sfx_cat_long_meow_1x
|
||||
- amzn_sfx_cat_meow_1x
|
||||
- amzn_sfx_cat_purr
|
||||
- amzn_sfx_cat_purr_meow
|
||||
- amzn_sfx_chicken_cluck
|
||||
- amzn_sfx_church_bell_1x
|
||||
- amzn_sfx_church_bells_ringing
|
||||
- amzn_sfx_clear_throat_ahem
|
||||
- amzn_sfx_clock_ticking
|
||||
- amzn_sfx_clock_ticking_long
|
||||
- amzn_sfx_copy_machine
|
||||
- amzn_sfx_cough
|
||||
- amzn_sfx_crow_caw_1x
|
||||
- amzn_sfx_crowd_applause
|
||||
- amzn_sfx_crowd_bar
|
||||
- amzn_sfx_crowd_bar_rowdy
|
||||
- amzn_sfx_crowd_boo
|
||||
- amzn_sfx_crowd_cheer_med
|
||||
- amzn_sfx_crowd_excited_cheer
|
||||
- amzn_sfx_dog_med_bark_1x
|
||||
- amzn_sfx_dog_med_bark_2x
|
||||
- amzn_sfx_dog_med_bark_growl
|
||||
- amzn_sfx_dog_med_growl_1x
|
||||
- amzn_sfx_dog_med_woof_1x
|
||||
- amzn_sfx_dog_small_bark_2x
|
||||
- amzn_sfx_door_open
|
||||
- amzn_sfx_door_shut
|
||||
- amzn_sfx_doorbell
|
||||
- amzn_sfx_doorbell_buzz
|
||||
- amzn_sfx_doorbell_chime
|
||||
- amzn_sfx_drinking_slurp
|
||||
- amzn_sfx_drum_and_cymbal
|
||||
- amzn_sfx_drum_comedy
|
||||
- amzn_sfx_earthquake_rumble
|
||||
- amzn_sfx_electric_guitar
|
||||
- amzn_sfx_electronic_beep
|
||||
- amzn_sfx_electronic_major_chord
|
||||
- amzn_sfx_elephant
|
||||
- amzn_sfx_elevator_bell_1x
|
||||
- amzn_sfx_elevator_open_bell
|
||||
- amzn_sfx_fairy_melodic_chimes
|
||||
- amzn_sfx_fairy_sparkle_chimes
|
||||
- amzn_sfx_faucet_drip
|
||||
- amzn_sfx_faucet_running
|
||||
- amzn_sfx_fireplace_crackle
|
||||
- amzn_sfx_fireworks
|
||||
- amzn_sfx_fireworks_firecrackers
|
||||
- amzn_sfx_fireworks_launch
|
||||
- amzn_sfx_fireworks_whistles
|
||||
- amzn_sfx_food_frying
|
||||
- amzn_sfx_footsteps
|
||||
- amzn_sfx_footsteps_muffled
|
||||
- amzn_sfx_ghost_spooky
|
||||
- amzn_sfx_glass_on_table
|
||||
- amzn_sfx_glasses_clink
|
||||
- amzn_sfx_horse_gallop_4x
|
||||
- amzn_sfx_horse_huff_whinny
|
||||
- amzn_sfx_horse_neigh
|
||||
- amzn_sfx_horse_neigh_low
|
||||
- amzn_sfx_horse_whinny
|
||||
- amzn_sfx_human_walking
|
||||
- amzn_sfx_jar_on_table_1x
|
||||
- amzn_sfx_kitchen_ambience
|
||||
- amzn_sfx_large_crowd_cheer
|
||||
- amzn_sfx_large_fire_crackling
|
||||
- amzn_sfx_laughter
|
||||
- amzn_sfx_laughter_giggle
|
||||
- amzn_sfx_lightning_strike
|
||||
- amzn_sfx_lion_roar
|
||||
- amzn_sfx_magic_blast_1x
|
||||
- amzn_sfx_monkey_calls_3x
|
||||
- amzn_sfx_monkey_chimp
|
||||
- amzn_sfx_monkeys_chatter
|
||||
- amzn_sfx_motorcycle_accelerate
|
||||
- amzn_sfx_motorcycle_engine_idle
|
||||
- amzn_sfx_motorcycle_engine_rev
|
||||
- amzn_sfx_musical_drone_intro
|
||||
- amzn_sfx_oars_splashing_rowboat
|
||||
- amzn_sfx_object_on_table_2x
|
||||
- amzn_sfx_ocean_wave_1x
|
||||
- amzn_sfx_ocean_wave_on_rocks_1x
|
||||
- amzn_sfx_ocean_wave_surf
|
||||
- amzn_sfx_people_walking
|
||||
- amzn_sfx_person_running
|
||||
- amzn_sfx_piano_note_1x
|
||||
- amzn_sfx_punch
|
||||
- amzn_sfx_rain
|
||||
- amzn_sfx_rain_on_roof
|
||||
- amzn_sfx_rain_thunder
|
||||
- amzn_sfx_rat_squeak_2x
|
||||
- amzn_sfx_rat_squeaks
|
||||
- amzn_sfx_raven_caw_1x
|
||||
- amzn_sfx_raven_caw_2x
|
||||
- amzn_sfx_restaurant_ambience
|
||||
- amzn_sfx_rooster_crow
|
||||
- amzn_sfx_scifi_air_escaping
|
||||
- amzn_sfx_scifi_alarm
|
||||
- amzn_sfx_scifi_alien_voice
|
||||
- amzn_sfx_scifi_boots_walking
|
||||
- amzn_sfx_scifi_close_large_explosion
|
||||
- amzn_sfx_scifi_door_open
|
||||
- amzn_sfx_scifi_engines_on
|
||||
- amzn_sfx_scifi_engines_on_large
|
||||
- amzn_sfx_scifi_engines_on_short_burst
|
||||
- amzn_sfx_scifi_explosion
|
||||
- amzn_sfx_scifi_explosion_2x
|
||||
- amzn_sfx_scifi_incoming_explosion
|
||||
- amzn_sfx_scifi_laser_gun_battle
|
||||
- amzn_sfx_scifi_laser_gun_fires
|
||||
- amzn_sfx_scifi_laser_gun_fires_large
|
||||
- amzn_sfx_scifi_long_explosion_1x
|
||||
- amzn_sfx_scifi_missile
|
||||
- amzn_sfx_scifi_motor_short_1x
|
||||
- amzn_sfx_scifi_open_airlock
|
||||
- amzn_sfx_scifi_radar_high_ping
|
||||
- amzn_sfx_scifi_radar_low
|
||||
- amzn_sfx_scifi_radar_medium
|
||||
- amzn_sfx_scifi_run_away
|
||||
- amzn_sfx_scifi_sheilds_up
|
||||
- amzn_sfx_scifi_short_low_explosion
|
||||
- amzn_sfx_scifi_small_whoosh_flyby
|
||||
- amzn_sfx_scifi_small_zoom_flyby
|
||||
- amzn_sfx_scifi_sonar_ping_3x
|
||||
- amzn_sfx_scifi_sonar_ping_4x
|
||||
- amzn_sfx_scifi_spaceship_flyby
|
||||
- amzn_sfx_scifi_timer_beep
|
||||
- amzn_sfx_scifi_zap_backwards
|
||||
- amzn_sfx_scifi_zap_electric
|
||||
- amzn_sfx_sheep_baa
|
||||
- amzn_sfx_sheep_bleat
|
||||
- amzn_sfx_silverware_clank
|
||||
- amzn_sfx_sirens
|
||||
- amzn_sfx_sleigh_bells
|
||||
- amzn_sfx_small_stream
|
||||
- amzn_sfx_sneeze
|
||||
- amzn_sfx_stream
|
||||
- amzn_sfx_strong_wind_desert
|
||||
- amzn_sfx_strong_wind_whistling
|
||||
- amzn_sfx_subway_leaving
|
||||
- amzn_sfx_subway_passing
|
||||
- amzn_sfx_subway_stopping
|
||||
- amzn_sfx_swoosh_cartoon_fast
|
||||
- amzn_sfx_swoosh_fast_1x
|
||||
- amzn_sfx_swoosh_fast_6x
|
||||
- amzn_sfx_test_tone
|
||||
- amzn_sfx_thunder_rumble
|
||||
- amzn_sfx_toilet_flush
|
||||
- amzn_sfx_trumpet_bugle
|
||||
- amzn_sfx_turkey_gobbling
|
||||
- amzn_sfx_typing_medium
|
||||
- amzn_sfx_typing_short
|
||||
- amzn_sfx_typing_typewriter
|
||||
- amzn_sfx_vacuum_off
|
||||
- amzn_sfx_vacuum_on
|
||||
- amzn_sfx_walking_in_mud
|
||||
- amzn_sfx_walking_in_snow
|
||||
- amzn_sfx_walking_on_grass
|
||||
- amzn_sfx_water_dripping
|
||||
- amzn_sfx_water_droplets
|
||||
- amzn_sfx_wind_strong_gusting
|
||||
- amzn_sfx_wind_whistling_desert
|
||||
- amzn_sfx_wings_flap_4x
|
||||
- amzn_sfx_wings_flap_fast
|
||||
- amzn_sfx_wolf_howl
|
||||
- amzn_sfx_wolf_young_howl
|
||||
- amzn_sfx_wooden_door
|
||||
- amzn_sfx_wooden_door_creaks_long
|
||||
- amzn_sfx_wooden_door_creaks_multiple
|
||||
- amzn_sfx_wooden_door_creaks_open
|
||||
- amzn_ui_sfx_gameshow_bridge
|
||||
- amzn_ui_sfx_gameshow_countdown_loop_32s_full
|
||||
- amzn_ui_sfx_gameshow_countdown_loop_64s_full
|
||||
- amzn_ui_sfx_gameshow_countdown_loop_64s_minimal
|
||||
- amzn_ui_sfx_gameshow_intro
|
||||
- amzn_ui_sfx_gameshow_negative_response
|
||||
- amzn_ui_sfx_gameshow_neutral_response
|
||||
- amzn_ui_sfx_gameshow_outro
|
||||
- amzn_ui_sfx_gameshow_player1
|
||||
- amzn_ui_sfx_gameshow_player2
|
||||
- amzn_ui_sfx_gameshow_player3
|
||||
- amzn_ui_sfx_gameshow_player4
|
||||
- amzn_ui_sfx_gameshow_positive_response
|
||||
- amzn_ui_sfx_gameshow_tally_negative
|
||||
- amzn_ui_sfx_gameshow_tally_positive
|
||||
- amzn_ui_sfx_gameshow_waiting_loop_30s
|
||||
- anchor
|
||||
- answering_machines
|
||||
- arcs_sparks
|
||||
- arrows_bows
|
||||
- baby
|
||||
- back_up_beeps
|
||||
- bars_restaurants
|
||||
- baseball
|
||||
- basketball
|
||||
- battles
|
||||
- beeps_tones
|
||||
- bell
|
||||
- bikes
|
||||
- billiards
|
||||
- board_games
|
||||
- body
|
||||
- boing
|
||||
- books
|
||||
- bow_wash
|
||||
- box
|
||||
- break_shatter_smash
|
||||
- breaks
|
||||
- brooms_mops
|
||||
- bullets
|
||||
- buses
|
||||
- buzz
|
||||
- buzz_hums
|
||||
- buzzers
|
||||
- buzzers_pistols
|
||||
- cables_metal
|
||||
- camera
|
||||
- cannons
|
||||
- car_alarm
|
||||
- car_alarms
|
||||
- car_cell_phones
|
||||
- carnivals_fairs
|
||||
- cars
|
||||
- casino
|
||||
- casinos
|
||||
- cellar
|
||||
- chimes
|
||||
- chimes_bells
|
||||
- chorus
|
||||
- christmas
|
||||
- church_bells
|
||||
- clock
|
||||
- cloth
|
||||
- concrete
|
||||
- construction
|
||||
- construction_factory
|
||||
- crashes
|
||||
- crowds
|
||||
- debris
|
||||
- dining_kitchens
|
||||
- dinosaurs
|
||||
- dripping
|
||||
- drops
|
||||
- electric
|
||||
- electrical
|
||||
- elevator
|
||||
- evolution_monsters
|
||||
- explosions
|
||||
- factory
|
||||
- falls
|
||||
- fax_scanner_copier
|
||||
- feedback_mics
|
||||
- fight
|
||||
- fire
|
||||
- fire_extinguisher
|
||||
- fireballs
|
||||
- fireworks
|
||||
- fishing_pole
|
||||
- flags
|
||||
- football
|
||||
- footsteps
|
||||
- futuristic
|
||||
- futuristic_ship
|
||||
- gameshow
|
||||
- gear
|
||||
- ghosts_demons
|
||||
- giant_monster
|
||||
- glass
|
||||
- glasses_clink
|
||||
- golf
|
||||
- gorilla
|
||||
- grenade_lanucher
|
||||
- griffen
|
||||
- gyms_locker_rooms
|
||||
- handgun_loading
|
||||
- handgun_shot
|
||||
- handle
|
||||
- hands
|
||||
- heartbeats_ekg
|
||||
- helicopter
|
||||
- high_tech
|
||||
- hit_punch_slap
|
||||
- hits
|
||||
- horns
|
||||
- horror
|
||||
- hot_tub_filling_up
|
||||
- human
|
||||
- human_vocals
|
||||
- hygene # codespell:ignore
|
||||
- ice_skating
|
||||
- ignitions
|
||||
- infantry
|
||||
- intro
|
||||
- jet
|
||||
- juggling
|
||||
- key_lock
|
||||
- kids
|
||||
- knocks
|
||||
- lab_equip
|
||||
- lacrosse
|
||||
- lamps_lanterns
|
||||
- leather
|
||||
- liquid_suction
|
||||
- locker_doors
|
||||
- machine_gun
|
||||
- magic_spells
|
||||
- medium_large_explosions
|
||||
- metal
|
||||
- modern_rings
|
||||
- money_coins
|
||||
- motorcycles
|
||||
- movement
|
||||
- moves
|
||||
- nature
|
||||
- oar_boat
|
||||
- pagers
|
||||
- paintball
|
||||
- paper
|
||||
- parachute
|
||||
- pay_phones
|
||||
- phone_beeps
|
||||
- pigmy_bats
|
||||
- pills
|
||||
- pour_water
|
||||
- power_up_down
|
||||
- printers
|
||||
- prison
|
||||
- public_space
|
||||
- racquetball
|
||||
- radios_static
|
||||
- rain
|
||||
- rc_airplane
|
||||
- rc_car
|
||||
- refrigerators_freezers
|
||||
- regular
|
||||
- respirator
|
||||
- rifle
|
||||
- roller_coaster
|
||||
- rollerskates_rollerblades
|
||||
- room_tones
|
||||
- ropes_climbing
|
||||
- rotary_rings
|
||||
- rowboat_canoe
|
||||
- rubber
|
||||
- running
|
||||
- sails
|
||||
- sand_gravel
|
||||
- screen_doors
|
||||
- screens
|
||||
- seats_stools
|
||||
- servos
|
||||
- shoes_boots
|
||||
- shotgun
|
||||
- shower
|
||||
- sink_faucet
|
||||
- sink_filling_water
|
||||
- sink_run_and_off
|
||||
- sink_water_splatter
|
||||
- sirens
|
||||
- skateboards
|
||||
- ski
|
||||
- skids_tires
|
||||
- sled
|
||||
- slides
|
||||
- small_explosions
|
||||
- snow
|
||||
- snowmobile
|
||||
- soldiers
|
||||
- splash_water
|
||||
- splashes_sprays
|
||||
- sports_whistles
|
||||
- squeaks
|
||||
- squeaky
|
||||
- stairs
|
||||
- steam
|
||||
- submarine_diesel
|
||||
- swing_doors
|
||||
- switches_levers
|
||||
- swords
|
||||
- tape
|
||||
- tape_machine
|
||||
- televisions_shows
|
||||
- tennis_pingpong
|
||||
- textile
|
||||
- throw
|
||||
- thunder
|
||||
- ticks
|
||||
- timer
|
||||
- toilet_flush
|
||||
- tone
|
||||
- tones_noises
|
||||
- toys
|
||||
- tractors
|
||||
- traffic
|
||||
- train
|
||||
- trucks_vans
|
||||
- turnstiles
|
||||
- typing
|
||||
- umbrella
|
||||
- underwater
|
||||
- vampires
|
||||
- various
|
||||
- video_tunes
|
||||
- volcano_earthquake
|
||||
- watches
|
||||
- water
|
||||
- water_running
|
||||
- werewolves
|
||||
- winches_gears
|
||||
- wind
|
||||
- wood
|
||||
- wood_boat
|
||||
- woosh
|
||||
- zap
|
||||
- zippers
|
||||
translation_key: sound
|
@@ -1,23 +1,21 @@
|
||||
{
|
||||
"common": {
|
||||
"data_code": "One-time password (OTP code)",
|
||||
"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."
|
||||
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.",
|
||||
"device_id_description": "The ID of the device to send the command to."
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{username}",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"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_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"country": "[%key:component::alexa_devices::common::data_description_country%]",
|
||||
"username": "[%key:component::alexa_devices::common::data_description_username%]",
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
@@ -45,7 +43,6 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.",
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
@@ -84,12 +81,532 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_sound": {
|
||||
"name": "Send sound",
|
||||
"description": "Sends a sound to a device",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"name": "Device",
|
||||
"description": "[%key:component::alexa_devices::common::device_id_description%]"
|
||||
},
|
||||
"sound": {
|
||||
"name": "Alexa Skill sound file",
|
||||
"description": "The sound file to play."
|
||||
},
|
||||
"sound_variant": {
|
||||
"name": "Sound variant",
|
||||
"description": "The variant of the sound to play."
|
||||
}
|
||||
}
|
||||
},
|
||||
"send_text_command": {
|
||||
"name": "Send text command",
|
||||
"description": "Sends a text command to a device",
|
||||
"fields": {
|
||||
"text_command": {
|
||||
"name": "Alexa text command",
|
||||
"description": "The text command to send."
|
||||
},
|
||||
"device_id": {
|
||||
"name": "Device",
|
||||
"description": "[%key:component::alexa_devices::common::device_id_description%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"sound": {
|
||||
"options": {
|
||||
"air_horn": "Air Horn",
|
||||
"air_horns": "Air Horns",
|
||||
"airboat": "Airboat",
|
||||
"airport": "Airport",
|
||||
"aliens": "Aliens",
|
||||
"amzn_sfx_airplane_takeoff_whoosh": "Airplane Takeoff Whoosh",
|
||||
"amzn_sfx_army_march_clank_7x": "Army March Clank 7x",
|
||||
"amzn_sfx_army_march_large_8x": "Army March Large 8x",
|
||||
"amzn_sfx_army_march_small_8x": "Army March Small 8x",
|
||||
"amzn_sfx_baby_big_cry": "Baby Big Cry",
|
||||
"amzn_sfx_baby_cry": "Baby Cry",
|
||||
"amzn_sfx_baby_fuss": "Baby Fuss",
|
||||
"amzn_sfx_battle_group_clanks": "Battle Group Clanks",
|
||||
"amzn_sfx_battle_man_grunts": "Battle Man Grunts",
|
||||
"amzn_sfx_battle_men_grunts": "Battle Men Grunts",
|
||||
"amzn_sfx_battle_men_horses": "Battle Men Horses",
|
||||
"amzn_sfx_battle_noisy_clanks": "Battle Noisy Clanks",
|
||||
"amzn_sfx_battle_yells_men": "Battle Yells Men",
|
||||
"amzn_sfx_battle_yells_men_run": "Battle Yells Men Run",
|
||||
"amzn_sfx_bear_groan_roar": "Bear Groan Roar",
|
||||
"amzn_sfx_bear_roar_grumble": "Bear Roar Grumble",
|
||||
"amzn_sfx_bear_roar_small": "Bear Roar Small",
|
||||
"amzn_sfx_beep_1x": "Beep 1x",
|
||||
"amzn_sfx_bell_med_chime": "Bell Med Chime",
|
||||
"amzn_sfx_bell_short_chime": "Bell Short Chime",
|
||||
"amzn_sfx_bell_timer": "Bell Timer",
|
||||
"amzn_sfx_bicycle_bell_ring": "Bicycle Bell Ring",
|
||||
"amzn_sfx_bird_chickadee_chirp_1x": "Bird Chickadee Chirp 1x",
|
||||
"amzn_sfx_bird_chickadee_chirps": "Bird Chickadee Chirps",
|
||||
"amzn_sfx_bird_forest": "Bird Forest",
|
||||
"amzn_sfx_bird_forest_short": "Bird Forest Short",
|
||||
"amzn_sfx_bird_robin_chirp_1x": "Bird Robin Chirp 1x",
|
||||
"amzn_sfx_boing_long_1x": "Boing Long 1x",
|
||||
"amzn_sfx_boing_med_1x": "Boing Med 1x",
|
||||
"amzn_sfx_boing_short_1x": "Boing Short 1x",
|
||||
"amzn_sfx_bus_drive_past": "Bus Drive Past",
|
||||
"amzn_sfx_buzz_electronic": "Buzz Electronic",
|
||||
"amzn_sfx_buzzer_loud_alarm": "Buzzer Loud Alarm",
|
||||
"amzn_sfx_buzzer_small": "Buzzer Small",
|
||||
"amzn_sfx_car_accelerate": "Car Accelerate",
|
||||
"amzn_sfx_car_accelerate_noisy": "Car Accelerate Noisy",
|
||||
"amzn_sfx_car_click_seatbelt": "Car Click Seatbelt",
|
||||
"amzn_sfx_car_close_door_1x": "Car Close Door 1x",
|
||||
"amzn_sfx_car_drive_past": "Car Drive Past",
|
||||
"amzn_sfx_car_honk_1x": "Car Honk 1x",
|
||||
"amzn_sfx_car_honk_2x": "Car Honk 2x",
|
||||
"amzn_sfx_car_honk_3x": "Car Honk 3x",
|
||||
"amzn_sfx_car_honk_long_1x": "Car Honk Long 1x",
|
||||
"amzn_sfx_car_into_driveway": "Car Into Driveway",
|
||||
"amzn_sfx_car_into_driveway_fast": "Car Into Driveway Fast",
|
||||
"amzn_sfx_car_slam_door_1x": "Car Slam Door 1x",
|
||||
"amzn_sfx_car_undo_seatbelt": "Car Undo Seatbelt",
|
||||
"amzn_sfx_cat_angry_meow_1x": "Cat Angry Meow 1x",
|
||||
"amzn_sfx_cat_angry_screech_1x": "Cat Angry Screech 1x",
|
||||
"amzn_sfx_cat_long_meow_1x": "Cat Long Meow 1x",
|
||||
"amzn_sfx_cat_meow_1x": "Cat Meow 1x",
|
||||
"amzn_sfx_cat_purr": "Cat Purr",
|
||||
"amzn_sfx_cat_purr_meow": "Cat Purr Meow",
|
||||
"amzn_sfx_chicken_cluck": "Chicken Cluck",
|
||||
"amzn_sfx_church_bell_1x": "Church Bell 1x",
|
||||
"amzn_sfx_church_bells_ringing": "Church Bells Ringing",
|
||||
"amzn_sfx_clear_throat_ahem": "Clear Throat Ahem",
|
||||
"amzn_sfx_clock_ticking": "Clock Ticking",
|
||||
"amzn_sfx_clock_ticking_long": "Clock Ticking Long",
|
||||
"amzn_sfx_copy_machine": "Copy Machine",
|
||||
"amzn_sfx_cough": "Cough",
|
||||
"amzn_sfx_crow_caw_1x": "Crow Caw 1x",
|
||||
"amzn_sfx_crowd_applause": "Crowd Applause",
|
||||
"amzn_sfx_crowd_bar": "Crowd Bar",
|
||||
"amzn_sfx_crowd_bar_rowdy": "Crowd Bar Rowdy",
|
||||
"amzn_sfx_crowd_boo": "Crowd Boo",
|
||||
"amzn_sfx_crowd_cheer_med": "Crowd Cheer Med",
|
||||
"amzn_sfx_crowd_excited_cheer": "Crowd Excited Cheer",
|
||||
"amzn_sfx_dog_med_bark_1x": "Dog Med Bark 1x",
|
||||
"amzn_sfx_dog_med_bark_2x": "Dog Med Bark 2x",
|
||||
"amzn_sfx_dog_med_bark_growl": "Dog Med Bark Growl",
|
||||
"amzn_sfx_dog_med_growl_1x": "Dog Med Growl 1x",
|
||||
"amzn_sfx_dog_med_woof_1x": "Dog Med Woof 1x",
|
||||
"amzn_sfx_dog_small_bark_2x": "Dog Small Bark 2x",
|
||||
"amzn_sfx_door_open": "Door Open",
|
||||
"amzn_sfx_door_shut": "Door Shut",
|
||||
"amzn_sfx_doorbell": "Doorbell",
|
||||
"amzn_sfx_doorbell_buzz": "Doorbell Buzz",
|
||||
"amzn_sfx_doorbell_chime": "Doorbell Chime",
|
||||
"amzn_sfx_drinking_slurp": "Drinking Slurp",
|
||||
"amzn_sfx_drum_and_cymbal": "Drum And Cymbal",
|
||||
"amzn_sfx_drum_comedy": "Drum Comedy",
|
||||
"amzn_sfx_earthquake_rumble": "Earthquake Rumble",
|
||||
"amzn_sfx_electric_guitar": "Electric Guitar",
|
||||
"amzn_sfx_electronic_beep": "Electronic Beep",
|
||||
"amzn_sfx_electronic_major_chord": "Electronic Major Chord",
|
||||
"amzn_sfx_elephant": "Elephant",
|
||||
"amzn_sfx_elevator_bell_1x": "Elevator Bell 1x",
|
||||
"amzn_sfx_elevator_open_bell": "Elevator Open Bell",
|
||||
"amzn_sfx_fairy_melodic_chimes": "Fairy Melodic Chimes",
|
||||
"amzn_sfx_fairy_sparkle_chimes": "Fairy Sparkle Chimes",
|
||||
"amzn_sfx_faucet_drip": "Faucet Drip",
|
||||
"amzn_sfx_faucet_running": "Faucet Running",
|
||||
"amzn_sfx_fireplace_crackle": "Fireplace Crackle",
|
||||
"amzn_sfx_fireworks": "Fireworks",
|
||||
"amzn_sfx_fireworks_firecrackers": "Fireworks Firecrackers",
|
||||
"amzn_sfx_fireworks_launch": "Fireworks Launch",
|
||||
"amzn_sfx_fireworks_whistles": "Fireworks Whistles",
|
||||
"amzn_sfx_food_frying": "Food Frying",
|
||||
"amzn_sfx_footsteps": "Footsteps",
|
||||
"amzn_sfx_footsteps_muffled": "Footsteps Muffled",
|
||||
"amzn_sfx_ghost_spooky": "Ghost Spooky",
|
||||
"amzn_sfx_glass_on_table": "Glass On Table",
|
||||
"amzn_sfx_glasses_clink": "Glasses Clink",
|
||||
"amzn_sfx_horse_gallop_4x": "Horse Gallop 4x",
|
||||
"amzn_sfx_horse_huff_whinny": "Horse Huff Whinny",
|
||||
"amzn_sfx_horse_neigh": "Horse Neigh",
|
||||
"amzn_sfx_horse_neigh_low": "Horse Neigh Low",
|
||||
"amzn_sfx_horse_whinny": "Horse Whinny",
|
||||
"amzn_sfx_human_walking": "Human Walking",
|
||||
"amzn_sfx_jar_on_table_1x": "Jar On Table 1x",
|
||||
"amzn_sfx_kitchen_ambience": "Kitchen Ambience",
|
||||
"amzn_sfx_large_crowd_cheer": "Large Crowd Cheer",
|
||||
"amzn_sfx_large_fire_crackling": "Large Fire Crackling",
|
||||
"amzn_sfx_laughter": "Laughter",
|
||||
"amzn_sfx_laughter_giggle": "Laughter Giggle",
|
||||
"amzn_sfx_lightning_strike": "Lightning Strike",
|
||||
"amzn_sfx_lion_roar": "Lion Roar",
|
||||
"amzn_sfx_magic_blast_1x": "Magic Blast 1x",
|
||||
"amzn_sfx_monkey_calls_3x": "Monkey Calls 3x",
|
||||
"amzn_sfx_monkey_chimp": "Monkey Chimp",
|
||||
"amzn_sfx_monkeys_chatter": "Monkeys Chatter",
|
||||
"amzn_sfx_motorcycle_accelerate": "Motorcycle Accelerate",
|
||||
"amzn_sfx_motorcycle_engine_idle": "Motorcycle Engine Idle",
|
||||
"amzn_sfx_motorcycle_engine_rev": "Motorcycle Engine Rev",
|
||||
"amzn_sfx_musical_drone_intro": "Musical Drone Intro",
|
||||
"amzn_sfx_oars_splashing_rowboat": "Oars Splashing Rowboat",
|
||||
"amzn_sfx_object_on_table_2x": "Object On Table 2x",
|
||||
"amzn_sfx_ocean_wave_1x": "Ocean Wave 1x",
|
||||
"amzn_sfx_ocean_wave_on_rocks_1x": "Ocean Wave On Rocks 1x",
|
||||
"amzn_sfx_ocean_wave_surf": "Ocean Wave Surf",
|
||||
"amzn_sfx_people_walking": "People Walking",
|
||||
"amzn_sfx_person_running": "Person Running",
|
||||
"amzn_sfx_piano_note_1x": "Piano Note 1x",
|
||||
"amzn_sfx_punch": "Punch",
|
||||
"amzn_sfx_rain": "Rain",
|
||||
"amzn_sfx_rain_on_roof": "Rain On Roof",
|
||||
"amzn_sfx_rain_thunder": "Rain Thunder",
|
||||
"amzn_sfx_rat_squeak_2x": "Rat Squeak 2x",
|
||||
"amzn_sfx_rat_squeaks": "Rat Squeaks",
|
||||
"amzn_sfx_raven_caw_1x": "Raven Caw 1x",
|
||||
"amzn_sfx_raven_caw_2x": "Raven Caw 2x",
|
||||
"amzn_sfx_restaurant_ambience": "Restaurant Ambience",
|
||||
"amzn_sfx_rooster_crow": "Rooster Crow",
|
||||
"amzn_sfx_scifi_air_escaping": "Scifi Air Escaping",
|
||||
"amzn_sfx_scifi_alarm": "Scifi Alarm",
|
||||
"amzn_sfx_scifi_alien_voice": "Scifi Alien Voice",
|
||||
"amzn_sfx_scifi_boots_walking": "Scifi Boots Walking",
|
||||
"amzn_sfx_scifi_close_large_explosion": "Scifi Close Large Explosion",
|
||||
"amzn_sfx_scifi_door_open": "Scifi Door Open",
|
||||
"amzn_sfx_scifi_engines_on": "Scifi Engines On",
|
||||
"amzn_sfx_scifi_engines_on_large": "Scifi Engines On Large",
|
||||
"amzn_sfx_scifi_engines_on_short_burst": "Scifi Engines On Short Burst",
|
||||
"amzn_sfx_scifi_explosion": "Scifi Explosion",
|
||||
"amzn_sfx_scifi_explosion_2x": "Scifi Explosion 2x",
|
||||
"amzn_sfx_scifi_incoming_explosion": "Scifi Incoming Explosion",
|
||||
"amzn_sfx_scifi_laser_gun_battle": "Scifi Laser Gun Battle",
|
||||
"amzn_sfx_scifi_laser_gun_fires": "Scifi Laser Gun Fires",
|
||||
"amzn_sfx_scifi_laser_gun_fires_large": "Scifi Laser Gun Fires Large",
|
||||
"amzn_sfx_scifi_long_explosion_1x": "Scifi Long Explosion 1x",
|
||||
"amzn_sfx_scifi_missile": "Scifi Missile",
|
||||
"amzn_sfx_scifi_motor_short_1x": "Scifi Motor Short 1x",
|
||||
"amzn_sfx_scifi_open_airlock": "Scifi Open Airlock",
|
||||
"amzn_sfx_scifi_radar_high_ping": "Scifi Radar High Ping",
|
||||
"amzn_sfx_scifi_radar_low": "Scifi Radar Low",
|
||||
"amzn_sfx_scifi_radar_medium": "Scifi Radar Medium",
|
||||
"amzn_sfx_scifi_run_away": "Scifi Run Away",
|
||||
"amzn_sfx_scifi_sheilds_up": "Scifi Sheilds Up",
|
||||
"amzn_sfx_scifi_short_low_explosion": "Scifi Short Low Explosion",
|
||||
"amzn_sfx_scifi_small_whoosh_flyby": "Scifi Small Whoosh Flyby",
|
||||
"amzn_sfx_scifi_small_zoom_flyby": "Scifi Small Zoom Flyby",
|
||||
"amzn_sfx_scifi_sonar_ping_3x": "Scifi Sonar Ping 3x",
|
||||
"amzn_sfx_scifi_sonar_ping_4x": "Scifi Sonar Ping 4x",
|
||||
"amzn_sfx_scifi_spaceship_flyby": "Scifi Spaceship Flyby",
|
||||
"amzn_sfx_scifi_timer_beep": "Scifi Timer Beep",
|
||||
"amzn_sfx_scifi_zap_backwards": "Scifi Zap Backwards",
|
||||
"amzn_sfx_scifi_zap_electric": "Scifi Zap Electric",
|
||||
"amzn_sfx_sheep_baa": "Sheep Baa",
|
||||
"amzn_sfx_sheep_bleat": "Sheep Bleat",
|
||||
"amzn_sfx_silverware_clank": "Silverware Clank",
|
||||
"amzn_sfx_sirens": "Sirens",
|
||||
"amzn_sfx_sleigh_bells": "Sleigh Bells",
|
||||
"amzn_sfx_small_stream": "Small Stream",
|
||||
"amzn_sfx_sneeze": "Sneeze",
|
||||
"amzn_sfx_stream": "Stream",
|
||||
"amzn_sfx_strong_wind_desert": "Strong Wind Desert",
|
||||
"amzn_sfx_strong_wind_whistling": "Strong Wind Whistling",
|
||||
"amzn_sfx_subway_leaving": "Subway Leaving",
|
||||
"amzn_sfx_subway_passing": "Subway Passing",
|
||||
"amzn_sfx_subway_stopping": "Subway Stopping",
|
||||
"amzn_sfx_swoosh_cartoon_fast": "Swoosh Cartoon Fast",
|
||||
"amzn_sfx_swoosh_fast_1x": "Swoosh Fast 1x",
|
||||
"amzn_sfx_swoosh_fast_6x": "Swoosh Fast 6x",
|
||||
"amzn_sfx_test_tone": "Test Tone",
|
||||
"amzn_sfx_thunder_rumble": "Thunder Rumble",
|
||||
"amzn_sfx_toilet_flush": "Toilet Flush",
|
||||
"amzn_sfx_trumpet_bugle": "Trumpet Bugle",
|
||||
"amzn_sfx_turkey_gobbling": "Turkey Gobbling",
|
||||
"amzn_sfx_typing_medium": "Typing Medium",
|
||||
"amzn_sfx_typing_short": "Typing Short",
|
||||
"amzn_sfx_typing_typewriter": "Typing Typewriter",
|
||||
"amzn_sfx_vacuum_off": "Vacuum Off",
|
||||
"amzn_sfx_vacuum_on": "Vacuum On",
|
||||
"amzn_sfx_walking_in_mud": "Walking In Mud",
|
||||
"amzn_sfx_walking_in_snow": "Walking In Snow",
|
||||
"amzn_sfx_walking_on_grass": "Walking On Grass",
|
||||
"amzn_sfx_water_dripping": "Water Dripping",
|
||||
"amzn_sfx_water_droplets": "Water Droplets",
|
||||
"amzn_sfx_wind_strong_gusting": "Wind Strong Gusting",
|
||||
"amzn_sfx_wind_whistling_desert": "Wind Whistling Desert",
|
||||
"amzn_sfx_wings_flap_4x": "Wings Flap 4x",
|
||||
"amzn_sfx_wings_flap_fast": "Wings Flap Fast",
|
||||
"amzn_sfx_wolf_howl": "Wolf Howl",
|
||||
"amzn_sfx_wolf_young_howl": "Wolf Young Howl",
|
||||
"amzn_sfx_wooden_door": "Wooden Door",
|
||||
"amzn_sfx_wooden_door_creaks_long": "Wooden Door Creaks Long",
|
||||
"amzn_sfx_wooden_door_creaks_multiple": "Wooden Door Creaks Multiple",
|
||||
"amzn_sfx_wooden_door_creaks_open": "Wooden Door Creaks Open",
|
||||
"amzn_ui_sfx_gameshow_bridge": "Gameshow Bridge",
|
||||
"amzn_ui_sfx_gameshow_countdown_loop_32s_full": "Gameshow Countdown Loop 32s Full",
|
||||
"amzn_ui_sfx_gameshow_countdown_loop_64s_full": "Gameshow Countdown Loop 64s Full",
|
||||
"amzn_ui_sfx_gameshow_countdown_loop_64s_minimal": "Gameshow Countdown Loop 64s Minimal",
|
||||
"amzn_ui_sfx_gameshow_intro": "Gameshow Intro",
|
||||
"amzn_ui_sfx_gameshow_negative_response": "Gameshow Negative Response",
|
||||
"amzn_ui_sfx_gameshow_neutral_response": "Gameshow Neutral Response",
|
||||
"amzn_ui_sfx_gameshow_outro": "Gameshow Outro",
|
||||
"amzn_ui_sfx_gameshow_player1": "Gameshow Player1",
|
||||
"amzn_ui_sfx_gameshow_player2": "Gameshow Player2",
|
||||
"amzn_ui_sfx_gameshow_player3": "Gameshow Player3",
|
||||
"amzn_ui_sfx_gameshow_player4": "Gameshow Player4",
|
||||
"amzn_ui_sfx_gameshow_positive_response": "Gameshow Positive Response",
|
||||
"amzn_ui_sfx_gameshow_tally_negative": "Gameshow Tally Negative",
|
||||
"amzn_ui_sfx_gameshow_tally_positive": "Gameshow Tally Positive",
|
||||
"amzn_ui_sfx_gameshow_waiting_loop_30s": "Gameshow Waiting Loop 30s",
|
||||
"anchor": "Anchor",
|
||||
"answering_machines": "Answering Machines",
|
||||
"arcs_sparks": "Arcs Sparks",
|
||||
"arrows_bows": "Arrows Bows",
|
||||
"baby": "Baby",
|
||||
"back_up_beeps": "Back Up Beeps",
|
||||
"bars_restaurants": "Bars Restaurants",
|
||||
"baseball": "Baseball",
|
||||
"basketball": "Basketball",
|
||||
"battles": "Battles",
|
||||
"beeps_tones": "Beeps Tones",
|
||||
"bell": "Bell",
|
||||
"bikes": "Bikes",
|
||||
"billiards": "Billiards",
|
||||
"board_games": "Board Games",
|
||||
"body": "Body",
|
||||
"boing": "Boing",
|
||||
"books": "Books",
|
||||
"bow_wash": "Bow Wash",
|
||||
"box": "Box",
|
||||
"break_shatter_smash": "Break Shatter Smash",
|
||||
"breaks": "Breaks",
|
||||
"brooms_mops": "Brooms Mops",
|
||||
"bullets": "Bullets",
|
||||
"buses": "Buses",
|
||||
"buzz": "Buzz",
|
||||
"buzz_hums": "Buzz Hums",
|
||||
"buzzers": "Buzzers",
|
||||
"buzzers_pistols": "Buzzers Pistols",
|
||||
"cables_metal": "Cables Metal",
|
||||
"camera": "Camera",
|
||||
"cannons": "Cannons",
|
||||
"car_alarm": "Car Alarm",
|
||||
"car_alarms": "Car Alarms",
|
||||
"car_cell_phones": "Car Cell Phones",
|
||||
"carnivals_fairs": "Carnivals Fairs",
|
||||
"cars": "Cars",
|
||||
"casino": "Casino",
|
||||
"casinos": "Casinos",
|
||||
"cellar": "Cellar",
|
||||
"chimes": "Chimes",
|
||||
"chimes_bells": "Chimes Bells",
|
||||
"chorus": "Chorus",
|
||||
"christmas": "Christmas",
|
||||
"church_bells": "Church Bells",
|
||||
"clock": "Clock",
|
||||
"cloth": "Cloth",
|
||||
"concrete": "Concrete",
|
||||
"construction": "Construction",
|
||||
"construction_factory": "Construction Factory",
|
||||
"crashes": "Crashes",
|
||||
"crowds": "Crowds",
|
||||
"debris": "Debris",
|
||||
"dining_kitchens": "Dining Kitchens",
|
||||
"dinosaurs": "Dinosaurs",
|
||||
"dripping": "Dripping",
|
||||
"drops": "Drops",
|
||||
"electric": "Electric",
|
||||
"electrical": "Electrical",
|
||||
"elevator": "Elevator",
|
||||
"evolution_monsters": "Evolution Monsters",
|
||||
"explosions": "Explosions",
|
||||
"factory": "Factory",
|
||||
"falls": "Falls",
|
||||
"fax_scanner_copier": "Fax Scanner Copier",
|
||||
"feedback_mics": "Feedback Mics",
|
||||
"fight": "Fight",
|
||||
"fire": "Fire",
|
||||
"fire_extinguisher": "Fire Extinguisher",
|
||||
"fireballs": "Fireballs",
|
||||
"fireworks": "Fireworks",
|
||||
"fishing_pole": "Fishing Pole",
|
||||
"flags": "Flags",
|
||||
"football": "Football",
|
||||
"footsteps": "Footsteps",
|
||||
"futuristic": "Futuristic",
|
||||
"futuristic_ship": "Futuristic Ship",
|
||||
"gameshow": "Gameshow",
|
||||
"gear": "Gear",
|
||||
"ghosts_demons": "Ghosts Demons",
|
||||
"giant_monster": "Giant Monster",
|
||||
"glass": "Glass",
|
||||
"glasses_clink": "Glasses Clink",
|
||||
"golf": "Golf",
|
||||
"gorilla": "Gorilla",
|
||||
"grenade_lanucher": "Grenade Lanucher",
|
||||
"griffen": "Griffen",
|
||||
"gyms_locker_rooms": "Gyms Locker Rooms",
|
||||
"handgun_loading": "Handgun Loading",
|
||||
"handgun_shot": "Handgun Shot",
|
||||
"handle": "Handle",
|
||||
"hands": "Hands",
|
||||
"heartbeats_ekg": "Heartbeats EKG",
|
||||
"helicopter": "Helicopter",
|
||||
"high_tech": "High Tech",
|
||||
"hit_punch_slap": "Hit Punch Slap",
|
||||
"hits": "Hits",
|
||||
"horns": "Horns",
|
||||
"horror": "Horror",
|
||||
"hot_tub_filling_up": "Hot Tub Filling Up",
|
||||
"human": "Human",
|
||||
"human_vocals": "Human Vocals",
|
||||
"hygene": "Hygene",
|
||||
"ice_skating": "Ice Skating",
|
||||
"ignitions": "Ignitions",
|
||||
"infantry": "Infantry",
|
||||
"intro": "Intro",
|
||||
"jet": "Jet",
|
||||
"juggling": "Juggling",
|
||||
"key_lock": "Key Lock",
|
||||
"kids": "Kids",
|
||||
"knocks": "Knocks",
|
||||
"lab_equip": "Lab Equip",
|
||||
"lacrosse": "Lacrosse",
|
||||
"lamps_lanterns": "Lamps Lanterns",
|
||||
"leather": "Leather",
|
||||
"liquid_suction": "Liquid Suction",
|
||||
"locker_doors": "Locker Doors",
|
||||
"machine_gun": "Machine Gun",
|
||||
"magic_spells": "Magic Spells",
|
||||
"medium_large_explosions": "Medium Large Explosions",
|
||||
"metal": "Metal",
|
||||
"modern_rings": "Modern Rings",
|
||||
"money_coins": "Money Coins",
|
||||
"motorcycles": "Motorcycles",
|
||||
"movement": "Movement",
|
||||
"moves": "Moves",
|
||||
"nature": "Nature",
|
||||
"oar_boat": "Oar Boat",
|
||||
"pagers": "Pagers",
|
||||
"paintball": "Paintball",
|
||||
"paper": "Paper",
|
||||
"parachute": "Parachute",
|
||||
"pay_phones": "Pay Phones",
|
||||
"phone_beeps": "Phone Beeps",
|
||||
"pigmy_bats": "Pigmy Bats",
|
||||
"pills": "Pills",
|
||||
"pour_water": "Pour Water",
|
||||
"power_up_down": "Power Up Down",
|
||||
"printers": "Printers",
|
||||
"prison": "Prison",
|
||||
"public_space": "Public Space",
|
||||
"racquetball": "Racquetball",
|
||||
"radios_static": "Radios Static",
|
||||
"rain": "Rain",
|
||||
"rc_airplane": "RC Airplane",
|
||||
"rc_car": "RC Car",
|
||||
"refrigerators_freezers": "Refrigerators Freezers",
|
||||
"regular": "Regular",
|
||||
"respirator": "Respirator",
|
||||
"rifle": "Rifle",
|
||||
"roller_coaster": "Roller Coaster",
|
||||
"rollerskates_rollerblades": "RollerSkates RollerBlades",
|
||||
"room_tones": "Room Tones",
|
||||
"ropes_climbing": "Ropes Climbing",
|
||||
"rotary_rings": "Rotary Rings",
|
||||
"rowboat_canoe": "Rowboat Canoe",
|
||||
"rubber": "Rubber",
|
||||
"running": "Running",
|
||||
"sails": "Sails",
|
||||
"sand_gravel": "Sand Gravel",
|
||||
"screen_doors": "Screen Doors",
|
||||
"screens": "Screens",
|
||||
"seats_stools": "Seats Stools",
|
||||
"servos": "Servos",
|
||||
"shoes_boots": "Shoes Boots",
|
||||
"shotgun": "Shotgun",
|
||||
"shower": "Shower",
|
||||
"sink_faucet": "Sink Faucet",
|
||||
"sink_filling_water": "Sink Filling Water",
|
||||
"sink_run_and_off": "Sink Run And Off",
|
||||
"sink_water_splatter": "Sink Water Splatter",
|
||||
"sirens": "Sirens",
|
||||
"skateboards": "Skateboards",
|
||||
"ski": "Ski",
|
||||
"skids_tires": "Skids Tires",
|
||||
"sled": "Sled",
|
||||
"slides": "Slides",
|
||||
"small_explosions": "Small Explosions",
|
||||
"snow": "Snow",
|
||||
"snowmobile": "Snowmobile",
|
||||
"soldiers": "Soldiers",
|
||||
"splash_water": "Splash Water",
|
||||
"splashes_sprays": "Splashes Sprays",
|
||||
"sports_whistles": "Sports Whistles",
|
||||
"squeaks": "Squeaks",
|
||||
"squeaky": "Squeaky",
|
||||
"stairs": "Stairs",
|
||||
"steam": "Steam",
|
||||
"submarine_diesel": "Submarine Diesel",
|
||||
"swing_doors": "Swing Doors",
|
||||
"switches_levers": "Switches Levers",
|
||||
"swords": "Swords",
|
||||
"tape": "Tape",
|
||||
"tape_machine": "Tape Machine",
|
||||
"televisions_shows": "Televisions Shows",
|
||||
"tennis_pingpong": "Tennis PingPong",
|
||||
"textile": "Textile",
|
||||
"throw": "Throw",
|
||||
"thunder": "Thunder",
|
||||
"ticks": "Ticks",
|
||||
"timer": "Timer",
|
||||
"toilet_flush": "Toilet Flush",
|
||||
"tone": "Tone",
|
||||
"tones_noises": "Tones Noises",
|
||||
"toys": "Toys",
|
||||
"tractors": "Tractors",
|
||||
"traffic": "Traffic",
|
||||
"train": "Train",
|
||||
"trucks_vans": "Trucks Vans",
|
||||
"turnstiles": "Turnstiles",
|
||||
"typing": "Typing",
|
||||
"umbrella": "Umbrella",
|
||||
"underwater": "Underwater",
|
||||
"vampires": "Vampires",
|
||||
"various": "Various",
|
||||
"video_tunes": "Video Tunes",
|
||||
"volcano_earthquake": "Volcano Earthquake",
|
||||
"watches": "Watches",
|
||||
"water": "Water",
|
||||
"water_running": "Water Running",
|
||||
"werewolves": "Werewolves",
|
||||
"winches_gears": "Winches Gears",
|
||||
"wind": "Wind",
|
||||
"wood": "Wood",
|
||||
"wood_boat": "Wood Boat",
|
||||
"woosh": "Woosh",
|
||||
"zap": "Zap",
|
||||
"zippers": "Zippers"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect_with_error": {
|
||||
"message": "Error connecting: {error}"
|
||||
},
|
||||
"cannot_retrieve_data_with_error": {
|
||||
"message": "Error retrieving data: {error}"
|
||||
},
|
||||
"device_serial_number_missing": {
|
||||
"message": "Device serial number missing: {device_id}"
|
||||
},
|
||||
"invalid_device_id": {
|
||||
"message": "Invalid device ID specified: {device_id}"
|
||||
},
|
||||
"invalid_sound_value": {
|
||||
"message": "Invalid sound {sound} with variant {variant} specified"
|
||||
},
|
||||
"entry_not_loaded": {
|
||||
"message": "Entry not loaded: {entry}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN, REQUEST_TIMEOUT
|
||||
|
||||
API_URL = "https://app.amber.com.au/developers"
|
||||
|
||||
@@ -64,7 +64,9 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
api = amberelectric.AmberApi(api_client)
|
||||
|
||||
try:
|
||||
sites: list[Site] = filter_sites(api.get_sites())
|
||||
sites: list[Site] = filter_sites(
|
||||
api.get_sites(_request_timeout=REQUEST_TIMEOUT)
|
||||
)
|
||||
except amberelectric.ApiException as api_exception:
|
||||
if api_exception.status == 403:
|
||||
self._errors[CONF_API_TOKEN] = "invalid_api_token"
|
||||
|
@@ -22,3 +22,5 @@ SERVICE_GET_FORECASTS = "get_forecasts"
|
||||
GENERAL_CHANNEL = "general"
|
||||
CONTROLLED_LOAD_CHANNEL = "controlled_load"
|
||||
FEED_IN_CHANNEL = "feed_in"
|
||||
|
||||
REQUEST_TIMEOUT = 15
|
||||
|
@@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import LOGGER
|
||||
from .const import LOGGER, REQUEST_TIMEOUT
|
||||
from .helpers import normalize_descriptor
|
||||
|
||||
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
|
||||
@@ -82,7 +82,11 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
|
||||
"grid": {},
|
||||
}
|
||||
try:
|
||||
data = self._api.get_current_prices(self.site_id, next=288)
|
||||
data = self._api.get_current_prices(
|
||||
self.site_id,
|
||||
next=288,
|
||||
_request_timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
intervals = [interval.actual_instance for interval in data]
|
||||
except ApiException as api_exception:
|
||||
raise UpdateFailed("Missing price data, skipping update") from api_exception
|
||||
|
@@ -430,7 +430,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
"model": device.model,
|
||||
"sw_version": device.sw_version,
|
||||
"hw_version": device.hw_version,
|
||||
"has_suggested_area": device.suggested_area is not None,
|
||||
"has_configuration_url": device.configuration_url is not None,
|
||||
"via_device": None,
|
||||
}
|
||||
|
@@ -81,11 +81,15 @@ async def async_update_options(
|
||||
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
"""Migrate integration entry structure."""
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
# Make sure we get enabled config entries first
|
||||
entries = sorted(
|
||||
hass.config_entries.async_entries(DOMAIN),
|
||||
key=lambda e: e.disabled_by is not None,
|
||||
)
|
||||
if not any(entry.version == 1 for entry in entries):
|
||||
return
|
||||
|
||||
api_keys_entries: dict[str, ConfigEntry] = {}
|
||||
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
@@ -99,30 +103,61 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
)
|
||||
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
||||
use_existing = True
|
||||
api_keys_entries[entry.data[CONF_API_KEY]] = entry
|
||||
all_disabled = all(
|
||||
e.disabled_by is not None
|
||||
for e in entries
|
||||
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
|
||||
)
|
||||
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
|
||||
|
||||
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
|
||||
parent_entry, all_disabled = 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_entity_id = 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 conversation_entity_id is not None:
|
||||
conversation_entity_entry = entity_registry.entities[conversation_entity_id]
|
||||
entity_disabled_by = conversation_entity_entry.disabled_by
|
||||
if (
|
||||
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
|
||||
and not all_disabled
|
||||
):
|
||||
# Device and entity registries don't update the disabled_by flag
|
||||
# when moving a device or entity from one config entry to another,
|
||||
# so we need to do it manually.
|
||||
entity_disabled_by = (
|
||||
er.RegistryEntryDisabler.DEVICE
|
||||
if device
|
||||
else er.RegistryEntryDisabler.USER
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
conversation_entity_id,
|
||||
config_entry_id=parent_entry.entry_id,
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
disabled_by=entity_disabled_by,
|
||||
new_unique_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
if device is not None:
|
||||
# Device and entity registries don't update the disabled_by flag when
|
||||
# moving a device or entity from one config entry to another, so we
|
||||
# need to do it manually.
|
||||
device_disabled_by = device.disabled_by
|
||||
if (
|
||||
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
|
||||
and not all_disabled
|
||||
):
|
||||
device_disabled_by = dr.DeviceEntryDisabler.USER
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
disabled_by=device_disabled_by,
|
||||
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
||||
add_config_subentry_id=subentry.subentry_id,
|
||||
add_config_entry_id=parent_entry.entry_id,
|
||||
@@ -147,7 +182,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||
title=DEFAULT_CONVERSATION_NAME,
|
||||
options={},
|
||||
version=2,
|
||||
minor_version=2,
|
||||
minor_version=3,
|
||||
)
|
||||
|
||||
|
||||
@@ -173,6 +208,38 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 2:
|
||||
# Fix migration where the disabled_by flag was not set correctly.
|
||||
# We can currently only correct this for enabled config entries,
|
||||
# because migration does not run for disabled config entries. This
|
||||
# is asserted in tests, and if that behavior is changed, we should
|
||||
# correct also disabled config entries.
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, entry.entry_id
|
||||
)
|
||||
if entry.disabled_by is None:
|
||||
# If the config entry is not disabled, we need to set the disabled_by
|
||||
# flag on devices to USER, and on entities to DEVICE, if they are set
|
||||
# to CONFIG_ENTRY.
|
||||
for device in devices:
|
||||
if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY:
|
||||
continue
|
||||
device_registry.async_update_device(
|
||||
device.id,
|
||||
disabled_by=dr.DeviceEntryDisabler.USER,
|
||||
)
|
||||
for entity in entity_entries:
|
||||
if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY:
|
||||
continue
|
||||
entity_registry.async_update_entity(
|
||||
entity.entity_id,
|
||||
disabled_by=er.RegistryEntryDisabler.DEVICE,
|
||||
)
|
||||
hass.config_entries.async_update_entry(entry, minor_version=3)
|
||||
|
||||
LOGGER.debug(
|
||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
@@ -75,7 +75,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Anthropic."""
|
||||
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyasuswrt import AsusWrtError
|
||||
|
||||
@@ -40,6 +40,9 @@ from .const import (
|
||||
SENSORS_CONNECTED_DEVICE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AsusWrtConfigEntry
|
||||
|
||||
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
@@ -52,10 +55,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AsusWrtSensorDataHandler:
|
||||
"""Data handler for AsusWrt sensor."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, api: AsusWrtBridge, entry: AsusWrtConfigEntry
|
||||
) -> None:
|
||||
"""Initialize a AsusWrt sensor data handler."""
|
||||
self._hass = hass
|
||||
self._api = api
|
||||
self._entry = entry
|
||||
self._connected_devices = 0
|
||||
|
||||
async def _get_connected_devices(self) -> dict[str, int]:
|
||||
@@ -91,6 +97,7 @@ class AsusWrtSensorDataHandler:
|
||||
update_method=method,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=SCAN_INTERVAL if should_poll else None,
|
||||
config_entry=self._entry,
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
@@ -321,7 +328,9 @@ class AsusWrtRouter:
|
||||
if self._sensors_data_handler:
|
||||
return
|
||||
|
||||
self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api)
|
||||
self._sensors_data_handler = AsusWrtSensorDataHandler(
|
||||
self.hass, self._api, self._entry
|
||||
)
|
||||
self._sensors_data_handler.update_device_count(self._connected_devices)
|
||||
|
||||
sensors_types = await self._api.async_get_available_sensors()
|
||||
|
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.0"]
|
||||
"requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"]
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==64"],
|
||||
"requirements": ["axis==65"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
@@ -1119,7 +1119,7 @@ class BackupManager:
|
||||
)
|
||||
if unavailable_agents:
|
||||
LOGGER.warning(
|
||||
"Backup agents %s are not available, will backupp to %s",
|
||||
"Backup agents %s are not available, will backup to %s",
|
||||
unavailable_agents,
|
||||
available_agents,
|
||||
)
|
||||
|
@@ -93,7 +93,7 @@
|
||||
}
|
||||
},
|
||||
"preset1": {
|
||||
"name": "Favourite 1",
|
||||
"name": "Favorite 1",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
@@ -107,7 +107,7 @@
|
||||
}
|
||||
},
|
||||
"preset2": {
|
||||
"name": "Favourite 2",
|
||||
"name": "Favorite 2",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
@@ -121,7 +121,7 @@
|
||||
}
|
||||
},
|
||||
"preset3": {
|
||||
"name": "Favourite 3",
|
||||
"name": "Favorite 3",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
@@ -135,7 +135,7 @@
|
||||
}
|
||||
},
|
||||
"preset4": {
|
||||
"name": "Favourite 4",
|
||||
"name": "Favorite 4",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
|
@@ -6,7 +6,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyblu==2.0.1"],
|
||||
"requirements": ["pyblu==2.0.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_musc._tcp.local."
|
||||
|
@@ -16,11 +16,11 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==1.0.1",
|
||||
"bleak-retry-connector==4.0.0",
|
||||
"bleak-retry-connector==4.0.2",
|
||||
"bluetooth-adapters==2.0.0",
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.2",
|
||||
"dbus-fast==2.44.2",
|
||||
"habluetooth==4.0.1"
|
||||
"dbus-fast==2.44.3",
|
||||
"habluetooth==4.0.2"
|
||||
]
|
||||
}
|
||||
|
@@ -69,12 +69,7 @@ class SHCEntity(SHCBaseEntity):
|
||||
manufacturer=device.manufacturer,
|
||||
model=device.device_model,
|
||||
name=device.name,
|
||||
via_device=(
|
||||
DOMAIN,
|
||||
device.parent_device_id
|
||||
if device.parent_device_id is not None
|
||||
else parent_id,
|
||||
),
|
||||
via_device=(DOMAIN, device.root_device_id),
|
||||
)
|
||||
super().__init__(device=device, parent_id=parent_id, entry_id=entry_id)
|
||||
|
||||
|
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bosch_shc",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["boschshcpy"],
|
||||
"requirements": ["boschshcpy==0.2.91"],
|
||||
"requirements": ["boschshcpy==0.2.107"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
@@ -64,6 +64,7 @@ class BroadlinkUpdateManager(ABC, Generic[_ApiT]):
|
||||
device.hass,
|
||||
_LOGGER,
|
||||
name=f"{device.name} ({device.api.model} at {device.api.host[0]})",
|
||||
config_entry=device.config,
|
||||
update_method=self.async_update,
|
||||
update_interval=self.SCAN_INTERVAL,
|
||||
)
|
||||
|
@@ -25,7 +25,7 @@
|
||||
"services": {
|
||||
"press": {
|
||||
"name": "Press",
|
||||
"description": "Press the button entity."
|
||||
"description": "Presses a button entity."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.3.1"]
|
||||
}
|
||||
|
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.108.0"],
|
||||
"requirements": ["hass-nabucasa==0.111.2"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/compensation",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["numpy==2.3.0"]
|
||||
"requirements": ["numpy==2.3.2"]
|
||||
}
|
||||
|
@@ -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.23"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.7.30"]
|
||||
}
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["cookidoo_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["cookidoo-api==0.12.2"]
|
||||
"requirements": ["cookidoo-api==0.14.0"]
|
||||
}
|
||||
|
@@ -75,7 +75,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: DatadogConfigEntry) -> b
|
||||
prefix = options[CONF_PREFIX]
|
||||
sample_rate = options[CONF_RATE]
|
||||
|
||||
statsd_client = DogStatsd(host=host, port=port, namespace=prefix)
|
||||
statsd_client = DogStatsd(
|
||||
host=host, port=port, namespace=prefix, disable_telemetry=True
|
||||
)
|
||||
entry.runtime_data = statsd_client
|
||||
|
||||
initialize(statsd_host=host, statsd_port=port)
|
||||
|
@@ -36,14 +36,14 @@ class DatadogConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle user config flow."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input:
|
||||
self._async_abort_entries_match(
|
||||
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
|
||||
)
|
||||
# Validate connection to Datadog Agent
|
||||
success = await validate_datadog_connection(
|
||||
self.hass,
|
||||
user_input,
|
||||
)
|
||||
self._async_abort_entries_match(
|
||||
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
|
||||
)
|
||||
if not success:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
@@ -58,7 +58,6 @@ class DatadogConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_RATE: user_input[CONF_RATE],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
@@ -107,7 +106,26 @@ class DatadogOptionsFlowHandler(OptionsFlow):
|
||||
options = self.config_entry.options
|
||||
|
||||
if user_input is None:
|
||||
user_input = {}
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_PREFIX,
|
||||
default=options.get(
|
||||
CONF_PREFIX, data.get(CONF_PREFIX, DEFAULT_PREFIX)
|
||||
),
|
||||
): str,
|
||||
vol.Required(
|
||||
CONF_RATE,
|
||||
default=options.get(
|
||||
CONF_RATE, data.get(CONF_RATE, DEFAULT_RATE)
|
||||
),
|
||||
): int,
|
||||
}
|
||||
),
|
||||
errors={},
|
||||
)
|
||||
|
||||
success = await validate_datadog_connection(
|
||||
self.hass,
|
||||
|
@@ -4,7 +4,7 @@ DOMAIN = "datadog"
|
||||
|
||||
CONF_RATE = "rate"
|
||||
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8125
|
||||
DEFAULT_PREFIX = "hass"
|
||||
DEFAULT_RATE = 1
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["datadog"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["datadog==0.15.0"]
|
||||
"requirements": ["datadog==0.52.0"]
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["denonavr"],
|
||||
"requirements": ["denonavr==1.1.1"],
|
||||
"requirements": ["denonavr==1.1.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Denon",
|
||||
|
@@ -7,45 +7,39 @@ from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from . import configure_mydevolo
|
||||
from .const import DOMAIN, SUPPORTED_MODEL_TYPES
|
||||
from .exceptions import CredentialsInvalid, UuidChanged
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||
)
|
||||
|
||||
|
||||
class DevoloHomeControlFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a devolo HomeControl config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_reauth_entry: ConfigEntry
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize devolo Home Control flow."""
|
||||
self.data_schema = {
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by the user."""
|
||||
if user_input is None:
|
||||
return self._show_form(step_id="user")
|
||||
try:
|
||||
return await self._connect_mydevolo(user_input)
|
||||
except CredentialsInvalid:
|
||||
return self._show_form(step_id="user", errors={"base": "invalid_auth"})
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
return await self._connect_mydevolo(user_input)
|
||||
except CredentialsInvalid:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
@@ -61,42 +55,47 @@ class DevoloHomeControlFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by zeroconf."""
|
||||
if user_input is None:
|
||||
return self._show_form(step_id="zeroconf_confirm")
|
||||
try:
|
||||
return await self._connect_mydevolo(user_input)
|
||||
except CredentialsInvalid:
|
||||
return self._show_form(
|
||||
step_id="zeroconf_confirm", errors={"base": "invalid_auth"}
|
||||
)
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
return await self._connect_mydevolo(user_input)
|
||||
except CredentialsInvalid:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauthentication."""
|
||||
self._reauth_entry = self._get_reauth_entry()
|
||||
self.data_schema = {
|
||||
vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by reauthentication."""
|
||||
if user_input is None:
|
||||
return self._show_form(step_id="reauth_confirm")
|
||||
try:
|
||||
return await self._connect_mydevolo(user_input)
|
||||
except CredentialsInvalid:
|
||||
return self._show_form(
|
||||
step_id="reauth_confirm", errors={"base": "invalid_auth"}
|
||||
)
|
||||
except UuidChanged:
|
||||
return self._show_form(
|
||||
step_id="reauth_confirm", errors={"base": "reauth_failed"}
|
||||
)
|
||||
errors: dict[str, str] = {}
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME, default=self.init_data[CONF_USERNAME]): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
return await self._connect_mydevolo(user_input)
|
||||
except CredentialsInvalid:
|
||||
errors["base"] = "invalid_auth"
|
||||
except UuidChanged:
|
||||
errors["base"] = "reauth_failed"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm", data_schema=data_schema, errors=errors
|
||||
)
|
||||
|
||||
async def _connect_mydevolo(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Connect to mydevolo."""
|
||||
@@ -119,21 +118,11 @@ class DevoloHomeControlFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
if self._reauth_entry.unique_id != uuid:
|
||||
if self.unique_id != uuid:
|
||||
# The old user and the new user are not the same. This could mess-up everything as all unique IDs might change.
|
||||
raise UuidChanged
|
||||
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
return self.async_update_reload_and_abort(
|
||||
self._reauth_entry, data=user_input, unique_id=uuid
|
||||
)
|
||||
|
||||
@callback
|
||||
def _show_form(
|
||||
self, step_id: str, errors: dict[str, str] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show the form to the user."""
|
||||
return self.async_show_form(
|
||||
step_id=step_id,
|
||||
data_schema=vol.Schema(self.data_schema),
|
||||
errors=errors if errors else {},
|
||||
reauth_entry, data=user_input, unique_id=uuid
|
||||
)
|
||||
|
@@ -8,6 +8,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["devolo_plc_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["devolo-plc-api==1.5.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
|
@@ -0,0 +1,84 @@
|
||||
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 have an options flow.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
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: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
A change of the IP address is covered by discovery-update-info and a change of the password is covered by reauthentication-flow. No other configuration options are available.
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration doesn't have any cases where raising an issue is needed.
|
||||
stale-devices:
|
||||
status: todo
|
||||
comment: |
|
||||
The tracked devices could be own devices with a manual delete option as the API cannot distinguish between stale devices and devices that are not home.
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
@@ -15,8 +15,8 @@
|
||||
],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.2.0",
|
||||
"aiodiscover==2.7.0",
|
||||
"aiodhcpwatcher==1.2.1",
|
||||
"aiodiscover==2.7.1",
|
||||
"cached-ipaddress==0.10.0"
|
||||
]
|
||||
}
|
||||
|
@@ -18,6 +18,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# If path is relative, we assume relative to Home Assistant config dir
|
||||
if not os.path.isabs(download_path):
|
||||
download_path = hass.config.path(download_path)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data={**entry.data, CONF_DOWNLOAD_DIR: download_path}
|
||||
)
|
||||
|
||||
if not await hass.async_add_executor_job(os.path.isdir, download_path):
|
||||
_LOGGER.error(
|
||||
|
@@ -11,6 +11,7 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
|
||||
@@ -34,24 +35,33 @@ def download_file(service: ServiceCall) -> None:
|
||||
|
||||
entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0]
|
||||
download_path = entry.data[CONF_DOWNLOAD_DIR]
|
||||
url: str = service.data[ATTR_URL]
|
||||
subdir: str | None = service.data.get(ATTR_SUBDIR)
|
||||
target_filename: str | None = service.data.get(ATTR_FILENAME)
|
||||
overwrite: bool = service.data[ATTR_OVERWRITE]
|
||||
|
||||
if subdir:
|
||||
# Check the path
|
||||
try:
|
||||
raise_if_invalid_path(subdir)
|
||||
except ValueError as err:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="subdir_invalid",
|
||||
translation_placeholders={"subdir": subdir},
|
||||
) from err
|
||||
if os.path.isabs(subdir):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="subdir_not_relative",
|
||||
translation_placeholders={"subdir": subdir},
|
||||
)
|
||||
|
||||
def do_download() -> None:
|
||||
"""Download the file."""
|
||||
final_path = None
|
||||
filename = target_filename
|
||||
try:
|
||||
url = service.data[ATTR_URL]
|
||||
|
||||
subdir = service.data.get(ATTR_SUBDIR)
|
||||
|
||||
filename = service.data.get(ATTR_FILENAME)
|
||||
|
||||
overwrite = service.data.get(ATTR_OVERWRITE)
|
||||
|
||||
if subdir:
|
||||
# Check the path
|
||||
raise_if_invalid_path(subdir)
|
||||
|
||||
final_path = None
|
||||
|
||||
req = requests.get(url, stream=True, timeout=10)
|
||||
|
||||
if req.status_code != HTTPStatus.OK:
|
||||
|
@@ -12,6 +12,14 @@
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"subdir_invalid": {
|
||||
"message": "Invalid subdirectory, got: {subdir}"
|
||||
},
|
||||
"subdir_not_relative": {
|
||||
"message": "Subdirectory must be relative, got: {subdir}"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"download_file": {
|
||||
"name": "Download file",
|
||||
|
@@ -4,10 +4,12 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from deebot_client.capabilities import CapabilityEvent
|
||||
from deebot_client.events.base import Event
|
||||
from deebot_client.events import Event
|
||||
from deebot_client.events.water_info import MopAttachedEvent
|
||||
from sucks import VacBot
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
@@ -16,7 +18,11 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import EcovacsConfigEntry
|
||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity
|
||||
from .entity import (
|
||||
EcovacsCapabilityEntityDescription,
|
||||
EcovacsDescriptionEntity,
|
||||
EcovacsLegacyEntity,
|
||||
)
|
||||
from .util import get_supported_entities
|
||||
|
||||
|
||||
@@ -47,12 +53,23 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add entities for passed config_entry in HA."""
|
||||
controller = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
get_supported_entities(
|
||||
config_entry.runtime_data, EcovacsBinarySensor, ENTITY_DESCRIPTIONS
|
||||
)
|
||||
)
|
||||
|
||||
legacy_entities = []
|
||||
for device in controller.legacy_devices:
|
||||
if not controller.legacy_entity_is_added(device, "battery_charging"):
|
||||
controller.add_legacy_entity(device, "battery_charging")
|
||||
legacy_entities.append(EcovacsLegacyBatteryChargingSensor(device))
|
||||
|
||||
if legacy_entities:
|
||||
async_add_entities(legacy_entities)
|
||||
|
||||
|
||||
class EcovacsBinarySensor[EventT: Event](
|
||||
EcovacsDescriptionEntity[CapabilityEvent[EventT]],
|
||||
@@ -71,3 +88,33 @@ class EcovacsBinarySensor[EventT: Event](
|
||||
self.async_write_ha_state()
|
||||
|
||||
self._subscribe(self._capability.event, on_event)
|
||||
|
||||
|
||||
class EcovacsLegacyBatteryChargingSensor(EcovacsLegacyEntity, BinarySensorEntity):
|
||||
"""Legacy battery charging sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: VacBot,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(device)
|
||||
self._attr_unique_id = f"{device.vacuum['did']}_battery_charging"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Set up the event listeners now that hass is ready."""
|
||||
self._event_listeners.append(
|
||||
self.device.statusEvents.subscribe(
|
||||
lambda _: self.schedule_update_ha_state()
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
if self.device.charge_status is None:
|
||||
return None
|
||||
return bool(self.device.is_charging)
|
||||
|
@@ -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.5.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.6.0"]
|
||||
}
|
||||
|
@@ -37,6 +37,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import EcovacsConfigEntry
|
||||
@@ -225,7 +226,7 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
async def _add_legacy_entities() -> None:
|
||||
async def _add_legacy_lifespan_entities() -> None:
|
||||
entities = []
|
||||
for device in controller.legacy_devices:
|
||||
for description in LEGACY_LIFESPAN_SENSORS:
|
||||
@@ -242,14 +243,21 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
def _fire_ecovacs_legacy_lifespan_event(_: Any) -> None:
|
||||
hass.create_task(_add_legacy_entities())
|
||||
hass.create_task(_add_legacy_lifespan_entities())
|
||||
|
||||
legacy_entities = []
|
||||
for device in controller.legacy_devices:
|
||||
config_entry.async_on_unload(
|
||||
device.lifespanEvents.subscribe(
|
||||
_fire_ecovacs_legacy_lifespan_event
|
||||
).unsubscribe
|
||||
)
|
||||
if not controller.legacy_entity_is_added(device, "battery_status"):
|
||||
controller.add_legacy_entity(device, "battery_status")
|
||||
legacy_entities.append(EcovacsLegacyBatterySensor(device))
|
||||
|
||||
if legacy_entities:
|
||||
async_add_entities(legacy_entities)
|
||||
|
||||
|
||||
class EcovacsSensor(
|
||||
@@ -344,6 +352,44 @@ class EcovacsErrorSensor(
|
||||
self._subscribe(self._capability.event, on_event)
|
||||
|
||||
|
||||
class EcovacsLegacyBatterySensor(EcovacsLegacyEntity, SensorEntity):
|
||||
"""Legacy battery sensor."""
|
||||
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
_attr_device_class = SensorDeviceClass.BATTERY
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: VacBot,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(device)
|
||||
self._attr_unique_id = f"{device.vacuum['did']}_battery_status"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Set up the event listeners now that hass is ready."""
|
||||
self._event_listeners.append(
|
||||
self.device.batteryEvents.subscribe(
|
||||
lambda _: self.schedule_update_ha_state()
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the value reported by the sensor."""
|
||||
if (status := self.device.battery_status) is not None:
|
||||
return status * 100 # type: ignore[no-any-return]
|
||||
return None
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return icon_for_battery_level(
|
||||
battery_level=self.native_value, charging=self.device.is_charging
|
||||
)
|
||||
|
||||
|
||||
class EcovacsLegacyLifespanSensor(EcovacsLegacyEntity, SensorEntity):
|
||||
"""Legacy Lifespan sensor."""
|
||||
|
||||
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from deebot_client.capabilities import Capabilities, DeviceType
|
||||
from deebot_client.device import Device
|
||||
from deebot_client.events import BatteryEvent, FanSpeedEvent, RoomsEvent, StateEvent
|
||||
from deebot_client.events import FanSpeedEvent, RoomsEvent, StateEvent
|
||||
from deebot_client.models import CleanAction, CleanMode, Room, State
|
||||
import sucks
|
||||
|
||||
@@ -22,7 +22,6 @@ from homeassistant.core import HomeAssistant, SupportsResponse
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import EcovacsConfigEntry
|
||||
@@ -71,8 +70,7 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity):
|
||||
|
||||
_attr_fan_speed_list = [sucks.FAN_SPEED_NORMAL, sucks.FAN_SPEED_HIGH]
|
||||
_attr_supported_features = (
|
||||
VacuumEntityFeature.BATTERY
|
||||
| VacuumEntityFeature.RETURN_HOME
|
||||
VacuumEntityFeature.RETURN_HOME
|
||||
| VacuumEntityFeature.CLEAN_SPOT
|
||||
| VacuumEntityFeature.STOP
|
||||
| VacuumEntityFeature.START
|
||||
@@ -89,11 +87,6 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity):
|
||||
lambda _: self.schedule_update_ha_state()
|
||||
)
|
||||
)
|
||||
self._event_listeners.append(
|
||||
self.device.batteryEvents.subscribe(
|
||||
lambda _: self.schedule_update_ha_state()
|
||||
)
|
||||
)
|
||||
self._event_listeners.append(
|
||||
self.device.lifespanEvents.subscribe(
|
||||
lambda _: self.schedule_update_ha_state()
|
||||
@@ -137,21 +130,6 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity):
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def battery_level(self) -> int | None:
|
||||
"""Return the battery level of the vacuum cleaner."""
|
||||
if self.device.battery_status is not None:
|
||||
return self.device.battery_status * 100 # type: ignore[no-any-return]
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def battery_icon(self) -> str:
|
||||
"""Return the battery icon for the vacuum cleaner."""
|
||||
return icon_for_battery_level(
|
||||
battery_level=self.battery_level, charging=self.device.is_charging
|
||||
)
|
||||
|
||||
@property
|
||||
def fan_speed(self) -> str | None:
|
||||
"""Return the fan speed of the vacuum cleaner."""
|
||||
@@ -238,7 +216,6 @@ class EcovacsVacuum(
|
||||
VacuumEntityFeature.PAUSE
|
||||
| VacuumEntityFeature.STOP
|
||||
| VacuumEntityFeature.RETURN_HOME
|
||||
| VacuumEntityFeature.BATTERY
|
||||
| VacuumEntityFeature.SEND_COMMAND
|
||||
| VacuumEntityFeature.LOCATE
|
||||
| VacuumEntityFeature.STATE
|
||||
@@ -265,10 +242,6 @@ class EcovacsVacuum(
|
||||
"""Set up the event listeners now that hass is ready."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
async def on_battery(event: BatteryEvent) -> None:
|
||||
self._attr_battery_level = event.value
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def on_rooms(event: RoomsEvent) -> None:
|
||||
self._rooms = event.rooms
|
||||
self.async_write_ha_state()
|
||||
@@ -277,7 +250,6 @@ class EcovacsVacuum(
|
||||
self._attr_activity = _STATE_TO_VACUUM_STATE[event.state]
|
||||
self.async_write_ha_state()
|
||||
|
||||
self._subscribe(self._capability.battery.event, on_battery)
|
||||
self._subscribe(self._capability.state.event, on_status)
|
||||
|
||||
if self._capability.fan_speed:
|
||||
|
@@ -1,5 +1,6 @@
|
||||
"""Data update coordinator for the Enigma2 integration."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from openwebif.api import OpenWebIfDevice, OpenWebIfStatus
|
||||
@@ -30,6 +31,8 @@ from .const import CONF_SOURCE_BOUQUET, DOMAIN
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
SETUP_TIMEOUT = 10
|
||||
|
||||
type Enigma2ConfigEntry = ConfigEntry[Enigma2UpdateCoordinator]
|
||||
|
||||
|
||||
@@ -79,7 +82,7 @@ class Enigma2UpdateCoordinator(DataUpdateCoordinator[OpenWebIfStatus]):
|
||||
async def _async_setup(self) -> None:
|
||||
"""Provide needed data to the device info."""
|
||||
|
||||
about = await self.device.get_about()
|
||||
about = await asyncio.wait_for(self.device.get_about(), timeout=SETUP_TIMEOUT)
|
||||
self.device.mac_address = about["info"]["ifaces"][0]["mac"]
|
||||
self.device_info["model"] = about["info"]["model"]
|
||||
self.device_info["manufacturer"] = about["info"]["brand"]
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pyenphase import Envoy
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
@@ -42,6 +44,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b
|
||||
},
|
||||
)
|
||||
|
||||
# register envoy before via_device is used
|
||||
device_registry = dr.async_get(hass)
|
||||
if TYPE_CHECKING:
|
||||
assert envoy.serial_number
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, envoy.serial_number)},
|
||||
manufacturer="Enphase",
|
||||
name=coordinator.name,
|
||||
model=envoy.envoy_model,
|
||||
sw_version=str(envoy.firmware),
|
||||
hw_version=envoy.part_number,
|
||||
serial_number=envoy.serial_number,
|
||||
)
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
@@ -116,6 +116,8 @@ async def async_get_config_entry_diagnostics(
|
||||
entities.append({"entity": entity_dict, "state": state_dict})
|
||||
device_dict = asdict(device)
|
||||
device_dict.pop("_cache", None)
|
||||
# This can be removed when suggested_area is removed from DeviceEntry
|
||||
device_dict.pop("_suggested_area")
|
||||
device_entities.append({"device": device_dict, "entities": entities})
|
||||
|
||||
# remove envoy serial
|
||||
|
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==2.2.2"],
|
||||
"requirements": ["pyenphase==2.2.3"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
@@ -51,6 +51,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info
|
||||
from .encryption_key_storage import async_get_encryption_key_storage
|
||||
from .entry_data import ESPHomeConfigEntry
|
||||
from .manager import async_replace_device
|
||||
|
||||
@@ -159,7 +160,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle reauthorization flow."""
|
||||
errors = {}
|
||||
|
||||
if await self._retrieve_encryption_key_from_dashboard():
|
||||
if (
|
||||
await self._retrieve_encryption_key_from_storage()
|
||||
or await self._retrieve_encryption_key_from_dashboard()
|
||||
):
|
||||
error = await self.fetch_device_info()
|
||||
if error is None:
|
||||
return await self._async_authenticate_or_add()
|
||||
@@ -226,9 +230,12 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
response = await self.fetch_device_info()
|
||||
self._noise_psk = None
|
||||
|
||||
# Try to retrieve an existing key from dashboard or storage.
|
||||
if (
|
||||
self._device_name
|
||||
and await self._retrieve_encryption_key_from_dashboard()
|
||||
) or (
|
||||
self._device_mac and await self._retrieve_encryption_key_from_storage()
|
||||
):
|
||||
response = await self.fetch_device_info()
|
||||
|
||||
@@ -284,6 +291,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._name = discovery_info.properties.get("friendly_name", device_name)
|
||||
self._host = discovery_info.host
|
||||
self._port = discovery_info.port
|
||||
self._device_mac = mac_address
|
||||
self._noise_required = bool(discovery_info.properties.get("api_encryption"))
|
||||
|
||||
# Check if already configured
|
||||
@@ -308,10 +316,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# Don't call _fetch_device_info() for ignored entries
|
||||
raise AbortFlow("already_configured")
|
||||
configured_host: str | None = entry.data.get(CONF_HOST)
|
||||
configured_port: int | None = entry.data.get(CONF_PORT)
|
||||
if configured_host == host and configured_port == port:
|
||||
configured_port: int = entry.data.get(CONF_PORT, DEFAULT_PORT)
|
||||
# When port is None (from DHCP discovery), only compare hosts
|
||||
if configured_host == host and (port is None or configured_port == port):
|
||||
# Don't probe to verify the mac is correct since
|
||||
# the host and port matches.
|
||||
# the host matches (and port matches if provided).
|
||||
raise AbortFlow("already_configured")
|
||||
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
|
||||
await self._fetch_device_info(host, port or configured_port, configured_psk)
|
||||
@@ -772,6 +781,26 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._noise_psk = noise_psk
|
||||
return True
|
||||
|
||||
async def _retrieve_encryption_key_from_storage(self) -> bool:
|
||||
"""Try to retrieve the encryption key from storage.
|
||||
|
||||
Return boolean if a key was retrieved.
|
||||
"""
|
||||
# Try to get MAC address from current flow state or reauth entry
|
||||
mac_address = self._device_mac
|
||||
if mac_address is None and self._reauth_entry is not None:
|
||||
# In reauth flow, get MAC from the existing entry's unique_id
|
||||
mac_address = self._reauth_entry.unique_id
|
||||
|
||||
assert mac_address is not None
|
||||
|
||||
storage = await async_get_encryption_key_storage(self.hass)
|
||||
if stored_key := await storage.async_get_key(mac_address):
|
||||
self._noise_psk = stored_key
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
|
@@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
|
||||
|
||||
DEFAULT_PORT: Final = 6053
|
||||
|
||||
STABLE_BLE_VERSION_STR = "2025.5.0"
|
||||
STABLE_BLE_VERSION_STR = "2025.8.0"
|
||||
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
|
||||
PROJECT_URLS = {
|
||||
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",
|
||||
|
94
homeassistant/components/esphome/encryption_key_storage.py
Normal file
94
homeassistant/components/esphome/encryption_key_storage.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Encryption key storage for ESPHome devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TypedDict
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ENCRYPTION_KEY_STORAGE_VERSION = 1
|
||||
ENCRYPTION_KEY_STORAGE_KEY = "esphome.encryption_keys"
|
||||
|
||||
|
||||
class EncryptionKeyData(TypedDict):
|
||||
"""Encryption key storage data."""
|
||||
|
||||
keys: dict[str, str] # MAC address -> base64 encoded key
|
||||
|
||||
|
||||
KEY_ENCRYPTION_STORAGE: HassKey[ESPHomeEncryptionKeyStorage] = HassKey(
|
||||
"esphome_encryption_key_storage"
|
||||
)
|
||||
|
||||
|
||||
class ESPHomeEncryptionKeyStorage:
|
||||
"""Storage for ESPHome encryption keys."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the encryption key storage."""
|
||||
self.hass = hass
|
||||
self._store = Store[EncryptionKeyData](
|
||||
hass,
|
||||
ENCRYPTION_KEY_STORAGE_VERSION,
|
||||
ENCRYPTION_KEY_STORAGE_KEY,
|
||||
encoder=JSONEncoder,
|
||||
)
|
||||
self._data: EncryptionKeyData | None = None
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load encryption keys from storage."""
|
||||
if self._data is None:
|
||||
data = await self._store.async_load()
|
||||
self._data = data or {"keys": {}}
|
||||
|
||||
async def async_save(self) -> None:
|
||||
"""Save encryption keys to storage."""
|
||||
if self._data is not None:
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
async def async_get_key(self, mac_address: str) -> str | None:
|
||||
"""Get encryption key for a MAC address."""
|
||||
await self.async_load()
|
||||
assert self._data is not None
|
||||
return self._data["keys"].get(mac_address.lower())
|
||||
|
||||
async def async_store_key(self, mac_address: str, key: str) -> None:
|
||||
"""Store encryption key for a MAC address."""
|
||||
await self.async_load()
|
||||
assert self._data is not None
|
||||
self._data["keys"][mac_address.lower()] = key
|
||||
await self.async_save()
|
||||
_LOGGER.debug(
|
||||
"Stored encryption key for device with MAC %s",
|
||||
mac_address,
|
||||
)
|
||||
|
||||
async def async_remove_key(self, mac_address: str) -> None:
|
||||
"""Remove encryption key for a MAC address."""
|
||||
await self.async_load()
|
||||
assert self._data is not None
|
||||
lower_mac_address = mac_address.lower()
|
||||
if lower_mac_address in self._data["keys"]:
|
||||
del self._data["keys"][lower_mac_address]
|
||||
await self.async_save()
|
||||
_LOGGER.debug(
|
||||
"Removed encryption key for device with MAC %s",
|
||||
mac_address,
|
||||
)
|
||||
|
||||
|
||||
@singleton(KEY_ENCRYPTION_STORAGE, async_=True)
|
||||
async def async_get_encryption_key_storage(
|
||||
hass: HomeAssistant,
|
||||
) -> ESPHomeEncryptionKeyStorage:
|
||||
"""Get the encryption key storage instance."""
|
||||
storage = ESPHomeEncryptionKeyStorage(hass)
|
||||
await storage.async_load()
|
||||
return storage
|
@@ -3,8 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
from functools import partial
|
||||
import logging
|
||||
import secrets
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||
|
||||
from aioesphomeapi import (
|
||||
@@ -68,6 +70,7 @@ from .const import (
|
||||
CONF_ALLOW_SERVICE_CALLS,
|
||||
CONF_BLUETOOTH_MAC_ADDRESS,
|
||||
CONF_DEVICE_NAME,
|
||||
CONF_NOISE_PSK,
|
||||
CONF_SUBSCRIBE_LOGS,
|
||||
DEFAULT_ALLOW_SERVICE_CALLS,
|
||||
DEFAULT_URL,
|
||||
@@ -78,6 +81,7 @@ from .const import (
|
||||
)
|
||||
from .dashboard import async_get_dashboard
|
||||
from .domain_data import DomainData
|
||||
from .encryption_key_storage import async_get_encryption_key_storage
|
||||
|
||||
# Import config flow so that it's added to the registry
|
||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||
@@ -85,9 +89,7 @@ from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||
DEVICE_CONFLICT_ISSUE_FORMAT = "device_conflict-{}"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined]
|
||||
SubscribeLogsResponse,
|
||||
)
|
||||
from aioesphomeapi.api_pb2 import SubscribeLogsResponse # type: ignore[attr-defined] # noqa: I001
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -515,6 +517,8 @@ class ESPHomeManager:
|
||||
assert api_version is not None, "API version must be set"
|
||||
entry_data.async_on_connect(device_info, api_version)
|
||||
|
||||
await self._handle_dynamic_encryption_key(device_info)
|
||||
|
||||
if device_info.name:
|
||||
reconnect_logic.name = device_info.name
|
||||
|
||||
@@ -618,6 +622,7 @@ class ESPHomeManager:
|
||||
),
|
||||
):
|
||||
return
|
||||
|
||||
if isinstance(err, InvalidEncryptionKeyAPIError):
|
||||
if (
|
||||
(received_name := err.received_name)
|
||||
@@ -648,6 +653,93 @@ class ESPHomeManager:
|
||||
return
|
||||
self.entry.async_start_reauth(self.hass)
|
||||
|
||||
async def _handle_dynamic_encryption_key(
|
||||
self, device_info: EsphomeDeviceInfo
|
||||
) -> None:
|
||||
"""Handle dynamic encryption keys.
|
||||
|
||||
If a device reports it supports encryption, but we connected without a key,
|
||||
we need to generate and store one.
|
||||
"""
|
||||
noise_psk: str | None = self.entry.data.get(CONF_NOISE_PSK)
|
||||
if noise_psk:
|
||||
# we're already connected with a noise PSK - nothing to do
|
||||
return
|
||||
|
||||
if not device_info.api_encryption_supported:
|
||||
# device does not support encryption - nothing to do
|
||||
return
|
||||
|
||||
# Connected to device without key and the device supports encryption
|
||||
storage = await async_get_encryption_key_storage(self.hass)
|
||||
|
||||
# First check if we have a key in storage for this device
|
||||
from_storage: bool = False
|
||||
if self.entry.unique_id and (
|
||||
stored_key := await storage.async_get_key(self.entry.unique_id)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Retrieved encryption key from storage for device %s",
|
||||
self.entry.unique_id,
|
||||
)
|
||||
# Use the stored key
|
||||
new_key = stored_key.encode()
|
||||
new_key_str = stored_key
|
||||
from_storage = True
|
||||
else:
|
||||
# No stored key found, generate a new one
|
||||
_LOGGER.debug(
|
||||
"Generating new encryption key for device %s", self.entry.unique_id
|
||||
)
|
||||
new_key = base64.b64encode(secrets.token_bytes(32))
|
||||
new_key_str = new_key.decode()
|
||||
|
||||
try:
|
||||
# Store the key on the device using the existing connection
|
||||
result = await self.cli.noise_encryption_set_key(new_key)
|
||||
except APIConnectionError as ex:
|
||||
_LOGGER.error(
|
||||
"Connection error while storing encryption key for device %s (%s): %s",
|
||||
self.entry.data.get(CONF_DEVICE_NAME, self.host),
|
||||
self.entry.unique_id,
|
||||
ex,
|
||||
)
|
||||
return
|
||||
else:
|
||||
if not result:
|
||||
_LOGGER.error(
|
||||
"Failed to set dynamic encryption key on device %s (%s)",
|
||||
self.entry.data.get(CONF_DEVICE_NAME, self.host),
|
||||
self.entry.unique_id,
|
||||
)
|
||||
return
|
||||
|
||||
# Key stored successfully on device
|
||||
assert self.entry.unique_id is not None
|
||||
|
||||
# Only store in storage if it was newly generated
|
||||
if not from_storage:
|
||||
await storage.async_store_key(self.entry.unique_id, new_key_str)
|
||||
|
||||
# Always update config entry
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.entry,
|
||||
data={**self.entry.data, CONF_NOISE_PSK: new_key_str},
|
||||
)
|
||||
|
||||
if from_storage:
|
||||
_LOGGER.info(
|
||||
"Set encryption key from storage on device %s (%s)",
|
||||
self.entry.data.get(CONF_DEVICE_NAME, self.host),
|
||||
self.entry.unique_id,
|
||||
)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Generated and stored encryption key for device %s (%s)",
|
||||
self.entry.data.get(CONF_DEVICE_NAME, self.host),
|
||||
self.entry.unique_id,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_handle_logging_changed(self, _event: Event) -> None:
|
||||
"""Handle when the logging level changes."""
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==37.0.2",
|
||||
"aioesphomeapi==37.2.2",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.1.0"
|
||||
],
|
||||
|
@@ -106,6 +106,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_logger_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
await self.logger_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -120,6 +121,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_meters_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -129,6 +131,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_ohmpilot_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -138,6 +141,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_power_flow_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -147,6 +151,7 @@ class FroniusSolarNet:
|
||||
solar_net=self,
|
||||
logger=_LOGGER,
|
||||
name=f"{DOMAIN}_storages_{self.host}",
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -206,6 +211,7 @@ class FroniusSolarNet:
|
||||
logger=_LOGGER,
|
||||
name=_inverter_name,
|
||||
inverter_info=_inverter_info,
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
await _coordinator.async_refresh()
|
||||
|
@@ -50,7 +50,7 @@ CONF_EXTRA_JS_URL_ES5 = "extra_js_url_es5"
|
||||
CONF_FRONTEND_REPO = "development_repo"
|
||||
CONF_JS_VERSION = "javascript_version"
|
||||
|
||||
DEFAULT_THEME_COLOR = "#03A9F4"
|
||||
DEFAULT_THEME_COLOR = "#2980b9"
|
||||
|
||||
|
||||
DATA_PANELS: HassKey[dict[str, Panel]] = HassKey("frontend_panels")
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250702.3"]
|
||||
"requirements": ["home-assistant-frontend==20250811.1"]
|
||||
}
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/garages_amsterdam",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["odp-amsterdam==6.1.1"]
|
||||
"requirements": ["odp-amsterdam==6.1.2"]
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@
|
||||
}
|
||||
},
|
||||
"confirm_discovery": {
|
||||
"description": "DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual."
|
||||
"description": "DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new IP address. Refer to your router's user manual."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
@@ -230,7 +230,7 @@ async def async_setup_entry(
|
||||
calendar_info = calendars[calendar_id]
|
||||
else:
|
||||
calendar_info = get_calendar_info(
|
||||
hass, calendar_item.dict(exclude_unset=True)
|
||||
hass, calendar_item.model_dump(exclude_unset=True)
|
||||
)
|
||||
new_calendars.append(calendar_info)
|
||||
|
||||
@@ -467,7 +467,7 @@ class GoogleCalendarEntity(
|
||||
else:
|
||||
start = DateOrDatetime(date=dtstart)
|
||||
end = DateOrDatetime(date=dtend)
|
||||
event = Event.parse_obj(
|
||||
event = Event.model_validate(
|
||||
{
|
||||
EVENT_SUMMARY: kwargs[EVENT_SUMMARY],
|
||||
"start": start,
|
||||
@@ -538,7 +538,7 @@ async def async_create_event(entity: GoogleCalendarEntity, call: ServiceCall) ->
|
||||
|
||||
if EVENT_IN in call.data:
|
||||
if EVENT_IN_DAYS in call.data[EVENT_IN]:
|
||||
now = datetime.now()
|
||||
now = datetime.now().date()
|
||||
|
||||
start_in = now + timedelta(days=call.data[EVENT_IN][EVENT_IN_DAYS])
|
||||
end_in = start_in + timedelta(days=1)
|
||||
@@ -547,7 +547,7 @@ async def async_create_event(entity: GoogleCalendarEntity, call: ServiceCall) ->
|
||||
end = DateOrDatetime(date=end_in)
|
||||
|
||||
elif EVENT_IN_WEEKS in call.data[EVENT_IN]:
|
||||
now = datetime.now()
|
||||
now = datetime.now().date()
|
||||
|
||||
start_in = now + timedelta(weeks=call.data[EVENT_IN][EVENT_IN_WEEKS])
|
||||
end_in = start_in + timedelta(days=1)
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.4"]
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==11.0.0"]
|
||||
}
|
||||
|
@@ -123,10 +123,10 @@
|
||||
},
|
||||
"ai_task_data": {
|
||||
"initiate_flow": {
|
||||
"user": "Add Generate data with AI service",
|
||||
"reconfigure": "Reconfigure Generate data with AI service"
|
||||
"user": "Add AI task",
|
||||
"reconfigure": "Reconfigure AI task"
|
||||
},
|
||||
"entry_type": "Generate data with AI service",
|
||||
"entry_type": "AI task",
|
||||
"step": {
|
||||
"set_options": {
|
||||
"data": {
|
||||
|
@@ -56,12 +56,12 @@ async def basic_group_options_schema(
|
||||
entity_selector: selector.Selector[Any] | vol.Schema
|
||||
if handler is None:
|
||||
entity_selector = selector.selector(
|
||||
{"entity": {"domain": domain, "multiple": True}}
|
||||
{"entity": {"domain": domain, "multiple": True, "reorder": True}}
|
||||
)
|
||||
else:
|
||||
entity_selector = entity_selector_without_own_entities(
|
||||
cast(SchemaOptionsFlowHandler, handler.parent_handler),
|
||||
selector.EntitySelectorConfig(domain=domain, multiple=True),
|
||||
selector.EntitySelectorConfig(domain=domain, multiple=True, reorder=True),
|
||||
)
|
||||
|
||||
return vol.Schema(
|
||||
@@ -78,7 +78,9 @@ def basic_group_config_schema(domain: str | list[str]) -> vol.Schema:
|
||||
{
|
||||
vol.Required("name"): selector.TextSelector(),
|
||||
vol.Required(CONF_ENTITIES): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=domain, multiple=True),
|
||||
selector.EntitySelectorConfig(
|
||||
domain=domain, multiple=True, reorder=True
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_HIDE_MEMBERS, default=False): selector.BooleanSelector(),
|
||||
}
|
||||
@@ -139,9 +141,7 @@ async def light_switch_options_schema(
|
||||
"""Generate options schema."""
|
||||
return (await basic_group_options_schema(domain, handler)).extend(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_ALL, default=False, description={"advanced": True}
|
||||
): selector.BooleanSelector(),
|
||||
vol.Required(CONF_ALL, default=False): selector.BooleanSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
|
@@ -21,12 +21,14 @@
|
||||
},
|
||||
"binary_sensor": {
|
||||
"title": "[%key:component::group::config::step::user::title%]",
|
||||
"description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on.",
|
||||
"data": {
|
||||
"all": "All entities",
|
||||
"entities": "Members",
|
||||
"hide_members": "Hide members",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"all": "If enabled, the group's state is on only if all members are on. If disabled, the group's state is on if any member is on."
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@@ -105,6 +107,9 @@
|
||||
"device_class": "Device class",
|
||||
"state_class": "State class",
|
||||
"unit_of_measurement": "Unit of measurement"
|
||||
},
|
||||
"data_description": {
|
||||
"ignore_non_numeric": "If enabled, the group's state is calculated if at least one member has a numerical value. If disabled, the group's state is calculated only if all group members have numerical values."
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
@@ -120,11 +125,13 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"binary_sensor": {
|
||||
"description": "[%key:component::group::config::step::binary_sensor::description%]",
|
||||
"data": {
|
||||
"all": "[%key:component::group::config::step::binary_sensor::data::all%]",
|
||||
"entities": "[%key:component::group::config::step::binary_sensor::data::entities%]",
|
||||
"hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]"
|
||||
},
|
||||
"data_description": {
|
||||
"all": "[%key:component::group::config::step::binary_sensor::data_description::all%]"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@@ -146,11 +153,13 @@
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"description": "[%key:component::group::config::step::binary_sensor::description%]",
|
||||
"data": {
|
||||
"all": "[%key:component::group::config::step::binary_sensor::data::all%]",
|
||||
"entities": "[%key:component::group::config::step::binary_sensor::data::entities%]",
|
||||
"hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]"
|
||||
},
|
||||
"data_description": {
|
||||
"all": "[%key:component::group::config::step::binary_sensor::data_description::all%]"
|
||||
}
|
||||
},
|
||||
"lock": {
|
||||
@@ -172,7 +181,6 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.",
|
||||
"data": {
|
||||
"ignore_non_numeric": "[%key:component::group::config::step::sensor::data::ignore_non_numeric%]",
|
||||
"entities": "[%key:component::group::config::step::binary_sensor::data::entities%]",
|
||||
@@ -182,14 +190,19 @@
|
||||
"device_class": "[%key:component::group::config::step::sensor::data::device_class%]",
|
||||
"state_class": "[%key:component::group::config::step::sensor::data::state_class%]",
|
||||
"unit_of_measurement": "[%key:component::group::config::step::sensor::data::unit_of_measurement%]"
|
||||
},
|
||||
"data_description": {
|
||||
"ignore_non_numeric": "[%key:component::group::config::step::sensor::data_description::ignore_non_numeric%]"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"description": "[%key:component::group::config::step::binary_sensor::description%]",
|
||||
"data": {
|
||||
"all": "[%key:component::group::config::step::binary_sensor::data::all%]",
|
||||
"entities": "[%key:component::group::config::step::binary_sensor::data::entities%]",
|
||||
"hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]"
|
||||
},
|
||||
"data_description": {
|
||||
"all": "[%key:component::group::config::step::binary_sensor::data_description::all%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,15 +7,7 @@ from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from habiticalib import (
|
||||
HabiticaClass,
|
||||
HabiticaException,
|
||||
NotAuthorizedError,
|
||||
Skill,
|
||||
TaskType,
|
||||
TooManyRequestsError,
|
||||
)
|
||||
from habiticalib import Habitica, HabiticaClass, Skill, TaskType
|
||||
|
||||
from homeassistant.components.button import (
|
||||
DOMAIN as BUTTON_DOMAIN,
|
||||
@@ -23,16 +15,11 @@ from homeassistant.components.button import (
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import ASSETS_URL, DOMAIN
|
||||
from .coordinator import (
|
||||
HabiticaConfigEntry,
|
||||
HabiticaData,
|
||||
HabiticaDataUpdateCoordinator,
|
||||
)
|
||||
from .coordinator import HabiticaConfigEntry, HabiticaData
|
||||
from .entity import HabiticaBase
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
@@ -42,7 +29,7 @@ PARALLEL_UPDATES = 1
|
||||
class HabiticaButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes Habitica button entity."""
|
||||
|
||||
press_fn: Callable[[HabiticaDataUpdateCoordinator], Any]
|
||||
press_fn: Callable[[Habitica], Any]
|
||||
available_fn: Callable[[HabiticaData], bool]
|
||||
class_needed: HabiticaClass | None = None
|
||||
entity_picture: str | None = None
|
||||
@@ -73,13 +60,13 @@ BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.RUN_CRON,
|
||||
translation_key=HabiticaButtonEntity.RUN_CRON,
|
||||
press_fn=lambda coordinator: coordinator.habitica.run_cron(),
|
||||
press_fn=lambda habitica: habitica.run_cron(),
|
||||
available_fn=lambda data: data.user.needsCron is True,
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.BUY_HEALTH_POTION,
|
||||
translation_key=HabiticaButtonEntity.BUY_HEALTH_POTION,
|
||||
press_fn=lambda coordinator: coordinator.habitica.buy_health_potion(),
|
||||
press_fn=lambda habitica: habitica.buy_health_potion(),
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.gp or 0) >= 25
|
||||
and (data.user.stats.hp or 0) < 50
|
||||
@@ -89,7 +76,7 @@ BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.ALLOCATE_ALL_STAT_POINTS,
|
||||
translation_key=HabiticaButtonEntity.ALLOCATE_ALL_STAT_POINTS,
|
||||
press_fn=lambda coordinator: coordinator.habitica.allocate_stat_points(),
|
||||
press_fn=lambda habitica: habitica.allocate_stat_points(),
|
||||
available_fn=(
|
||||
lambda data: data.user.preferences.automaticAllocation is True
|
||||
and (data.user.stats.points or 0) > 0
|
||||
@@ -98,7 +85,7 @@ BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.REVIVE,
|
||||
translation_key=HabiticaButtonEntity.REVIVE,
|
||||
press_fn=lambda coordinator: coordinator.habitica.revive(),
|
||||
press_fn=lambda habitica: habitica.revive(),
|
||||
available_fn=lambda data: data.user.stats.hp == 0,
|
||||
),
|
||||
)
|
||||
@@ -108,9 +95,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.MPHEAL,
|
||||
translation_key=HabiticaButtonEntity.MPHEAL,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.ETHEREAL_SURGE)
|
||||
),
|
||||
press_fn=lambda habitica: habitica.cast_skill(Skill.ETHEREAL_SURGE),
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 12
|
||||
and (data.user.stats.mp or 0) >= 30
|
||||
@@ -121,7 +106,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.EARTH,
|
||||
translation_key=HabiticaButtonEntity.EARTH,
|
||||
press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.EARTHQUAKE),
|
||||
press_fn=lambda habitica: habitica.cast_skill(Skill.EARTHQUAKE),
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 13
|
||||
and (data.user.stats.mp or 0) >= 35
|
||||
@@ -132,9 +117,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.FROST,
|
||||
translation_key=HabiticaButtonEntity.FROST,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.CHILLING_FROST)
|
||||
),
|
||||
press_fn=lambda habitica: habitica.cast_skill(Skill.CHILLING_FROST),
|
||||
# chilling frost can only be cast once per day (streaks buff is false)
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 14
|
||||
@@ -147,9 +130,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.DEFENSIVE_STANCE,
|
||||
translation_key=HabiticaButtonEntity.DEFENSIVE_STANCE,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.DEFENSIVE_STANCE)
|
||||
),
|
||||
press_fn=lambda habitica: habitica.cast_skill(Skill.DEFENSIVE_STANCE),
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 12
|
||||
and (data.user.stats.mp or 0) >= 25
|
||||
@@ -160,9 +141,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.VALOROUS_PRESENCE,
|
||||
translation_key=HabiticaButtonEntity.VALOROUS_PRESENCE,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.VALOROUS_PRESENCE)
|
||||
),
|
||||
press_fn=lambda habitica: habitica.cast_skill(Skill.VALOROUS_PRESENCE),
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 13
|
||||
and (data.user.stats.mp or 0) >= 20
|
||||
@@ -173,9 +152,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.INTIMIDATE,
|
||||
translation_key=HabiticaButtonEntity.INTIMIDATE,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.INTIMIDATING_GAZE)
|
||||
),
|
||||
press_fn=lambda habitica: habitica.cast_skill(Skill.INTIMIDATING_GAZE),
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 14
|
||||
and (data.user.stats.mp or 0) >= 15
|
||||
@@ -186,11 +163,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.TOOLS_OF_TRADE,
|
||||
translation_key=HabiticaButtonEntity.TOOLS_OF_TRADE,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.habitica.cast_skill(
|
||||
Skill.TOOLS_OF_THE_TRADE
|
||||
)
|
||||
),
|
||||
press_fn=lambda habitica: habitica.cast_skill(Skill.TOOLS_OF_THE_TRADE),
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 13
|
||||
and (data.user.stats.mp or 0) >= 25
|
||||
@@ -201,7 +174,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.STEALTH,
|
||||
translation_key=HabiticaButtonEntity.STEALTH,
|
||||
press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.STEALTH),
|
||||
press_fn=lambda habitica: habitica.cast_skill(Skill.STEALTH),
|
||||
# Stealth buffs stack and it can only be cast if the amount of
|
||||
# buffs is smaller than the amount of unfinished dailies
|
||||
available_fn=(
|
||||
@@ -224,9 +197,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.HEAL,
|
||||
translation_key=HabiticaButtonEntity.HEAL,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.HEALING_LIGHT)
|
||||
),
|
||||
press_fn=lambda habitica: habitica.cast_skill(Skill.HEALING_LIGHT),
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 11
|
||||
and (data.user.stats.mp or 0) >= 15
|
||||
@@ -238,11 +209,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.BRIGHTNESS,
|
||||
translation_key=HabiticaButtonEntity.BRIGHTNESS,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.habitica.cast_skill(
|
||||
Skill.SEARING_BRIGHTNESS
|
||||
)
|
||||
),
|
||||
press_fn=lambda habitica: habitica.cast_skill(Skill.SEARING_BRIGHTNESS),
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 12
|
||||
and (data.user.stats.mp or 0) >= 15
|
||||
@@ -253,9 +220,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.PROTECT_AURA,
|
||||
translation_key=HabiticaButtonEntity.PROTECT_AURA,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.PROTECTIVE_AURA)
|
||||
),
|
||||
press_fn=lambda habitica: habitica.cast_skill(Skill.PROTECTIVE_AURA),
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 13
|
||||
and (data.user.stats.mp or 0) >= 30
|
||||
@@ -266,7 +231,7 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabiticaButtonEntity.HEAL_ALL,
|
||||
translation_key=HabiticaButtonEntity.HEAL_ALL,
|
||||
press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.BLESSING),
|
||||
press_fn=lambda habitica: habitica.cast_skill(Skill.BLESSING),
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 14
|
||||
and (data.user.stats.mp or 0) >= 25
|
||||
@@ -332,33 +297,9 @@ class HabiticaButton(HabiticaBase, ButtonEntity):
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
try:
|
||||
await self.entity_description.press_fn(self.coordinator)
|
||||
except TooManyRequestsError as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
translation_placeholders={"retry_after": str(e.retry_after)},
|
||||
) from e
|
||||
except NotAuthorizedError as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_unallowed",
|
||||
) from e
|
||||
except HabiticaException as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_exception",
|
||||
translation_placeholders={"reason": e.error.message},
|
||||
) from e
|
||||
except ClientError as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_exception",
|
||||
translation_placeholders={"reason": str(e)},
|
||||
) from e
|
||||
else:
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
await self.coordinator.execute(self.entity_description.press_fn)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
@@ -164,7 +164,6 @@ class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data={
|
||||
CONF_API_USER: str(login.id),
|
||||
CONF_API_KEY: login.apiToken,
|
||||
CONF_NAME: user.profile.name, # needed for api_call action
|
||||
CONF_URL: DEFAULT_URL,
|
||||
CONF_VERIFY_SSL: True,
|
||||
},
|
||||
@@ -200,7 +199,6 @@ class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data={
|
||||
**user_input,
|
||||
CONF_URL: user_input.get(CONF_URL, DEFAULT_URL),
|
||||
CONF_NAME: user.profile.name, # needed for api_call action
|
||||
},
|
||||
)
|
||||
|
||||
|
@@ -23,12 +23,12 @@ from habiticalib import (
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@@ -106,12 +106,6 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
|
||||
translation_placeholders={"reason": str(e)},
|
||||
) from e
|
||||
|
||||
if not self.config_entry.data.get(CONF_NAME):
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry,
|
||||
data={**self.config_entry.data, CONF_NAME: user.data.profile.name},
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> HabiticaData:
|
||||
try:
|
||||
user = (await self.habitica.get_user()).data
|
||||
@@ -137,19 +131,22 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
|
||||
else:
|
||||
return HabiticaData(user=user, tasks=tasks + completed_todos)
|
||||
|
||||
async def execute(
|
||||
self, func: Callable[[HabiticaDataUpdateCoordinator], Any]
|
||||
) -> None:
|
||||
async def execute(self, func: Callable[[Habitica], Any]) -> None:
|
||||
"""Execute an API call."""
|
||||
|
||||
try:
|
||||
await func(self)
|
||||
await func(self.habitica)
|
||||
except TooManyRequestsError as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
translation_placeholders={"retry_after": str(e.retry_after)},
|
||||
) from e
|
||||
except NotAuthorizedError as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_unallowed",
|
||||
) from e
|
||||
except HabiticaException as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
|
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_URL
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -37,7 +37,7 @@ class HabiticaBase(CoordinatorEntity[HabiticaDataUpdateCoordinator]):
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer=MANUFACTURER,
|
||||
model=NAME,
|
||||
name=coordinator.config_entry.data[CONF_NAME],
|
||||
name=coordinator.data.user.profile.name,
|
||||
configuration_url=(
|
||||
URL(coordinator.config_entry.data[CONF_URL])
|
||||
/ "profile"
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["habiticalib"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["habiticalib==0.4.0"]
|
||||
"requirements": ["habiticalib==0.4.2"]
|
||||
}
|
||||
|
@@ -7,6 +7,8 @@ from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from habiticalib import Habitica
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
SwitchEntity,
|
||||
@@ -15,11 +17,7 @@ from homeassistant.components.switch import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import (
|
||||
HabiticaConfigEntry,
|
||||
HabiticaData,
|
||||
HabiticaDataUpdateCoordinator,
|
||||
)
|
||||
from .coordinator import HabiticaConfigEntry, HabiticaData
|
||||
from .entity import HabiticaBase
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
@@ -29,8 +27,8 @@ PARALLEL_UPDATES = 1
|
||||
class HabiticaSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Describes Habitica switch entity."""
|
||||
|
||||
turn_on_fn: Callable[[HabiticaDataUpdateCoordinator], Any]
|
||||
turn_off_fn: Callable[[HabiticaDataUpdateCoordinator], Any]
|
||||
turn_on_fn: Callable[[Habitica], Any]
|
||||
turn_off_fn: Callable[[Habitica], Any]
|
||||
is_on_fn: Callable[[HabiticaData], bool | None]
|
||||
|
||||
|
||||
@@ -45,8 +43,8 @@ SWTICH_DESCRIPTIONS: tuple[HabiticaSwitchEntityDescription, ...] = (
|
||||
key=HabiticaSwitchEntity.SLEEP,
|
||||
translation_key=HabiticaSwitchEntity.SLEEP,
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
turn_on_fn=lambda coordinator: coordinator.habitica.toggle_sleep(),
|
||||
turn_off_fn=lambda coordinator: coordinator.habitica.toggle_sleep(),
|
||||
turn_on_fn=lambda habitica: habitica.toggle_sleep(),
|
||||
turn_off_fn=lambda habitica: habitica.toggle_sleep(),
|
||||
is_on_fn=lambda data: data.user.preferences.sleep,
|
||||
),
|
||||
)
|
||||
|
@@ -61,18 +61,19 @@ PLACEHOLDER_KEY_REASON = "reason"
|
||||
|
||||
UNSUPPORTED_REASONS = {
|
||||
"apparmor",
|
||||
"cgroup_version",
|
||||
"connectivity_check",
|
||||
"content_trust",
|
||||
"dbus",
|
||||
"dns_server",
|
||||
"docker_configuration",
|
||||
"docker_version",
|
||||
"cgroup_version",
|
||||
"job_conditions",
|
||||
"lxc",
|
||||
"network_manager",
|
||||
"os",
|
||||
"os_agent",
|
||||
"os_version",
|
||||
"restart_policy",
|
||||
"software",
|
||||
"source_mods",
|
||||
@@ -80,15 +81,18 @@ UNSUPPORTED_REASONS = {
|
||||
"systemd",
|
||||
"systemd_journal",
|
||||
"systemd_resolved",
|
||||
"virtualization_image",
|
||||
}
|
||||
# Some unsupported reasons also mark the system as unhealthy. If the unsupported reason
|
||||
# provides no additional information beyond the unhealthy one then skip that repair.
|
||||
UNSUPPORTED_SKIP_REPAIR = {"privileged"}
|
||||
UNHEALTHY_REASONS = {
|
||||
"docker",
|
||||
"supervisor",
|
||||
"setup",
|
||||
"duplicate_os_installation",
|
||||
"oserror_bad_message",
|
||||
"privileged",
|
||||
"setup",
|
||||
"supervisor",
|
||||
"untrusted",
|
||||
}
|
||||
|
||||
|
@@ -116,35 +116,43 @@
|
||||
},
|
||||
"unhealthy": {
|
||||
"title": "Unhealthy system - {reason}",
|
||||
"description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this."
|
||||
"description": "System is currently unhealthy due to {reason}. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unhealthy_docker": {
|
||||
"title": "Unhealthy system - Docker misconfigured",
|
||||
"description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this."
|
||||
"description": "System is currently unhealthy because Docker is configured incorrectly. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unhealthy_supervisor": {
|
||||
"title": "Unhealthy system - Supervisor update failed",
|
||||
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this."
|
||||
"unhealthy_duplicate_os_installation": {
|
||||
"description": "System is currently unhealthy because it has detected multiple Home Assistant OS installations. For troubleshooting information, select Learn more.",
|
||||
"title": "Unhealthy system - Duplicate Home Assistant OS installation"
|
||||
},
|
||||
"unhealthy_setup": {
|
||||
"title": "Unhealthy system - Setup failed",
|
||||
"description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this."
|
||||
"unhealthy_oserror_bad_message": {
|
||||
"description": "System is currently unhealthy because the operating system has reported an OS error: Bad message. For troubleshooting information, select Learn more.",
|
||||
"title": "Unhealthy system - Operating System error: Bad message"
|
||||
},
|
||||
"unhealthy_privileged": {
|
||||
"title": "Unhealthy system - Not privileged",
|
||||
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this."
|
||||
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unhealthy_setup": {
|
||||
"title": "Unhealthy system - Setup failed",
|
||||
"description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unhealthy_supervisor": {
|
||||
"title": "Unhealthy system - Supervisor update failed",
|
||||
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unhealthy_untrusted": {
|
||||
"title": "Unhealthy system - Untrusted code",
|
||||
"description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this."
|
||||
"description": "System is currently unhealthy because it has detected untrusted code or images in use. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported": {
|
||||
"title": "Unsupported system - {reason}",
|
||||
"description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported due to {reason}. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_apparmor": {
|
||||
"title": "Unsupported system - AppArmor issues",
|
||||
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_cgroup_version": {
|
||||
"title": "Unsupported system - CGroup version",
|
||||
@@ -152,23 +160,23 @@
|
||||
},
|
||||
"unsupported_connectivity_check": {
|
||||
"title": "Unsupported system - Connectivity check disabled",
|
||||
"description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_content_trust": {
|
||||
"title": "Unsupported system - Content-trust check disabled",
|
||||
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_dbus": {
|
||||
"title": "Unsupported system - D-Bus issues",
|
||||
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_dns_server": {
|
||||
"title": "Unsupported system - DNS server issues",
|
||||
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_docker_configuration": {
|
||||
"title": "Unsupported system - Docker misconfigured",
|
||||
"description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because the Docker daemon is running in an unexpected way. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_docker_version": {
|
||||
"title": "Unsupported system - Docker version",
|
||||
@@ -176,15 +184,15 @@
|
||||
},
|
||||
"unsupported_job_conditions": {
|
||||
"title": "Unsupported system - Protections disabled",
|
||||
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_lxc": {
|
||||
"title": "Unsupported system - LXC detected",
|
||||
"description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because it is being run in an LXC virtual machine. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_network_manager": {
|
||||
"title": "Unsupported system - Network Manager issues",
|
||||
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_os": {
|
||||
"title": "Unsupported system - Operating System",
|
||||
@@ -192,39 +200,43 @@
|
||||
},
|
||||
"unsupported_os_agent": {
|
||||
"title": "Unsupported system - OS-Agent issues",
|
||||
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_restart_policy": {
|
||||
"title": "Unsupported system - Container restart policy",
|
||||
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_software": {
|
||||
"title": "Unsupported system - Unsupported software",
|
||||
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_source_mods": {
|
||||
"title": "Unsupported system - Supervisor source modifications",
|
||||
"description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Supervisor source code has been modified. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_supervisor_version": {
|
||||
"title": "Unsupported system - Supervisor version",
|
||||
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_systemd": {
|
||||
"title": "Unsupported system - Systemd issues",
|
||||
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_systemd_journal": {
|
||||
"title": "Unsupported system - Systemd Journal issues",
|
||||
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_systemd_resolved": {
|
||||
"title": "Unsupported system - Systemd-Resolved issues",
|
||||
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_virtualization_image": {
|
||||
"title": "Unsupported system - Incorrect OS image for virtualization",
|
||||
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this."
|
||||
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. For troubleshooting information, select Learn more."
|
||||
},
|
||||
"unsupported_os_version": {
|
||||
"title": "Unsupported system - Home Assistant OS version",
|
||||
"description": "System is unsupported because the Home Assistant OS version in use is not supported. For troubleshooting information, select Learn more."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
@@ -2,11 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
|
||||
from .const import TRAVEL_MODE_PUBLIC
|
||||
from .const import CONF_TRAFFIC_MODE, TRAVEL_MODE_PUBLIC
|
||||
from .coordinator import (
|
||||
HereConfigEntry,
|
||||
HERERoutingDataUpdateCoordinator,
|
||||
@@ -15,6 +17,8 @@ from .coordinator import (
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: HereConfigEntry) -> bool:
|
||||
"""Set up HERE Travel Time from a config entry."""
|
||||
@@ -43,3 +47,28 @@ async def async_unload_entry(
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: HereConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate an old config entry."""
|
||||
|
||||
if config_entry.version == 1 and config_entry.minor_version == 1:
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s.%s",
|
||||
config_entry.version,
|
||||
config_entry.minor_version,
|
||||
)
|
||||
options = dict(config_entry.options)
|
||||
options[CONF_TRAFFIC_MODE] = True
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, options=options, version=1, minor_version=2
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s successful",
|
||||
config_entry.version,
|
||||
config_entry.minor_version,
|
||||
)
|
||||
return True
|
||||
|
@@ -33,6 +33,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
EntitySelector,
|
||||
LocationSelector,
|
||||
TimeSelector,
|
||||
@@ -50,6 +51,7 @@ from .const import (
|
||||
CONF_ORIGIN_LATITUDE,
|
||||
CONF_ORIGIN_LONGITUDE,
|
||||
CONF_ROUTE_MODE,
|
||||
CONF_TRAFFIC_MODE,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
ROUTE_MODE_FASTEST,
|
||||
@@ -65,6 +67,7 @@ DEFAULT_OPTIONS = {
|
||||
CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
|
||||
CONF_ARRIVAL_TIME: None,
|
||||
CONF_DEPARTURE_TIME: None,
|
||||
CONF_TRAFFIC_MODE: True,
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +105,7 @@ class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for HERE Travel Time."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Init Config Flow."""
|
||||
@@ -307,7 +311,9 @@ class HERETravelTimeOptionsFlow(OptionsFlow):
|
||||
"""Manage the HERE Travel Time options."""
|
||||
if user_input is not None:
|
||||
self._config = user_input
|
||||
return await self.async_step_time_menu()
|
||||
if self._config[CONF_TRAFFIC_MODE]:
|
||||
return await self.async_step_time_menu()
|
||||
return self.async_create_entry(title="", data=self._config)
|
||||
|
||||
schema = self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
@@ -318,12 +324,21 @@ class HERETravelTimeOptionsFlow(OptionsFlow):
|
||||
CONF_ROUTE_MODE, DEFAULT_OPTIONS[CONF_ROUTE_MODE]
|
||||
),
|
||||
): vol.In(ROUTE_MODES),
|
||||
vol.Optional(
|
||||
CONF_TRAFFIC_MODE,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_TRAFFIC_MODE, DEFAULT_OPTIONS[CONF_TRAFFIC_MODE]
|
||||
),
|
||||
): BooleanSelector(),
|
||||
}
|
||||
),
|
||||
{
|
||||
CONF_ROUTE_MODE: self.config_entry.options.get(
|
||||
CONF_ROUTE_MODE, DEFAULT_OPTIONS[CONF_ROUTE_MODE]
|
||||
),
|
||||
CONF_TRAFFIC_MODE: self.config_entry.options.get(
|
||||
CONF_TRAFFIC_MODE, DEFAULT_OPTIONS[CONF_TRAFFIC_MODE]
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
@@ -19,6 +19,7 @@ CONF_ARRIVAL = "arrival"
|
||||
CONF_DEPARTURE = "departure"
|
||||
CONF_ARRIVAL_TIME = "arrival_time"
|
||||
CONF_DEPARTURE_TIME = "departure_time"
|
||||
CONF_TRAFFIC_MODE = "traffic_mode"
|
||||
|
||||
DEFAULT_NAME = "HERE Travel Time"
|
||||
|
||||
|
@@ -13,6 +13,7 @@ from here_routing import (
|
||||
Return,
|
||||
RoutingMode,
|
||||
Spans,
|
||||
TrafficMode,
|
||||
TransportMode,
|
||||
)
|
||||
import here_transit
|
||||
@@ -44,6 +45,7 @@ from .const import (
|
||||
CONF_ORIGIN_LATITUDE,
|
||||
CONF_ORIGIN_LONGITUDE,
|
||||
CONF_ROUTE_MODE,
|
||||
CONF_TRAFFIC_MODE,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
ROUTE_MODE_FASTEST,
|
||||
@@ -87,7 +89,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Requesting route for origin: %s, destination: %s, route_mode: %s,"
|
||||
" mode: %s, arrival: %s, departure: %s"
|
||||
" mode: %s, arrival: %s, departure: %s, traffic_mode: %s"
|
||||
),
|
||||
params.origin,
|
||||
params.destination,
|
||||
@@ -95,6 +97,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
TransportMode(params.travel_mode),
|
||||
params.arrival,
|
||||
params.departure,
|
||||
params.traffic_mode,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -109,6 +112,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
routing_mode=params.route_mode,
|
||||
arrival_time=params.arrival,
|
||||
departure_time=params.departure,
|
||||
traffic_mode=params.traffic_mode,
|
||||
return_values=[Return.POLYINE, Return.SUMMARY],
|
||||
spans=[Spans.NAMES],
|
||||
)
|
||||
@@ -350,6 +354,11 @@ def prepare_parameters(
|
||||
if config_entry.options[CONF_ROUTE_MODE] == ROUTE_MODE_FASTEST
|
||||
else RoutingMode.SHORT
|
||||
)
|
||||
traffic_mode = (
|
||||
TrafficMode.DISABLED
|
||||
if config_entry.options[CONF_TRAFFIC_MODE] is False
|
||||
else TrafficMode.DEFAULT
|
||||
)
|
||||
|
||||
return HERETravelTimeAPIParams(
|
||||
destination=destination,
|
||||
@@ -358,6 +367,7 @@ def prepare_parameters(
|
||||
route_mode=route_mode,
|
||||
arrival=arrival,
|
||||
departure=departure,
|
||||
traffic_mode=traffic_mode,
|
||||
)
|
||||
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user