forked from home-assistant/core
Compare commits
507 Commits
knx_servic
...
analytics-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98cff0bd74 | ||
|
|
b3a32b5f59 | ||
|
|
d7d2b7ad76 | ||
|
|
0eea3176d6 | ||
|
|
4f20977a8e | ||
|
|
5bd63bb56b | ||
|
|
f7103da818 | ||
|
|
bf4922a7ef | ||
|
|
6f7eac5c6d | ||
|
|
d6e73a89f3 | ||
|
|
269aefd405 | ||
|
|
a6865f1639 | ||
|
|
f55aa0b86e | ||
|
|
02b34f05aa | ||
|
|
37f42707e5 | ||
|
|
17f3ba1434 | ||
|
|
31dcc25ba5 | ||
|
|
4da93f6a5e | ||
|
|
5ed7d32749 | ||
|
|
ab5b9dbdc9 | ||
|
|
3b28bf07d1 | ||
|
|
b626c9b450 | ||
|
|
5430eca93e | ||
|
|
b41c477f44 | ||
|
|
5900413c08 | ||
|
|
c2ceab741f | ||
|
|
45ff4940eb | ||
|
|
9c8a15cb64 | ||
|
|
b09e54c961 | ||
|
|
f44b7e202a | ||
|
|
0f535e979f | ||
|
|
4c2c01b4f6 | ||
|
|
b1d48fe9a2 | ||
|
|
b1dfc3cd23 | ||
|
|
696efe349e | ||
|
|
6a32722acc | ||
|
|
8eaec56c6b | ||
|
|
60d3c9342d | ||
|
|
2bd5039f28 | ||
|
|
8b1b14a704 | ||
|
|
5e674ce1d0 | ||
|
|
3656bcf752 | ||
|
|
39093fc2bc | ||
|
|
efa5838be4 | ||
|
|
1c6ad2fa66 | ||
|
|
af144e1b77 | ||
|
|
b451bfed81 | ||
|
|
3e32c50936 | ||
|
|
208b15637a | ||
|
|
c958cce769 | ||
|
|
602ec54579 | ||
|
|
fa2bfc5d9d | ||
|
|
94f906b34c | ||
|
|
a4f210379d | ||
|
|
3db6d82904 | ||
|
|
b8ddfd642e | ||
|
|
39f418f2d2 | ||
|
|
9fbd484dfe | ||
|
|
1773f2aadc | ||
|
|
cb1b72d6ba | ||
|
|
f5a2ec961d | ||
|
|
bf40e77d65 | ||
|
|
568bdef61f | ||
|
|
2303521778 | ||
|
|
3bf2946d13 | ||
|
|
484e5cb3e8 | ||
|
|
fbe8b6c34d | ||
|
|
4e7397dc9d | ||
|
|
a6189106e1 | ||
|
|
ed6123a3e6 | ||
|
|
0cd5deaa3f | ||
|
|
6c047e2678 | ||
|
|
405a480cae | ||
|
|
b4e69bab71 | ||
|
|
db81edfb2b | ||
|
|
24829bc44f | ||
|
|
c8594045df | ||
|
|
ea3f9b971f | ||
|
|
380974eed4 | ||
|
|
8151403bf6 | ||
|
|
16f5e76f00 | ||
|
|
b6b178cac0 | ||
|
|
0f020366e3 | ||
|
|
27a19be369 | ||
|
|
0c166eb307 | ||
|
|
79d73c28a7 | ||
|
|
2aed01b530 | ||
|
|
3fb0d61271 | ||
|
|
599acaf514 | ||
|
|
5f4103a4a7 | ||
|
|
c7c72231c7 | ||
|
|
6887a4419e | ||
|
|
db5cb6233c | ||
|
|
963829712d | ||
|
|
46ceccfbb3 | ||
|
|
aaf3039967 | ||
|
|
2509f18def | ||
|
|
a1e2d79613 | ||
|
|
96ba5c3983 | ||
|
|
041282190a | ||
|
|
8cdd5de75c | ||
|
|
a95c232f11 | ||
|
|
c9aba288b4 | ||
|
|
35a9d502af | ||
|
|
409c8783fe | ||
|
|
3adc3d7732 | ||
|
|
ec19712388 | ||
|
|
2c89e89c84 | ||
|
|
e602a464db | ||
|
|
ffc0651d89 | ||
|
|
7162efd836 | ||
|
|
8e7d782102 | ||
|
|
dc2028f99c | ||
|
|
f12ba5f7a9 | ||
|
|
45fb21e32d | ||
|
|
ecbb417736 | ||
|
|
3a59a862d5 | ||
|
|
e34fab0045 | ||
|
|
7254ebe0e3 | ||
|
|
b43bc3f32d | ||
|
|
ca3d13b5cc | ||
|
|
c8818bcce3 | ||
|
|
b234b5937a | ||
|
|
1bdef0f2f7 | ||
|
|
56fb61bd6f | ||
|
|
2c7d0b8909 | ||
|
|
cbb8d76da7 | ||
|
|
cce925c06c | ||
|
|
505a4bfc34 | ||
|
|
58e151966c | ||
|
|
8a6c9b7afc | ||
|
|
e72e2071b0 | ||
|
|
5d3af27928 | ||
|
|
5dc0bedbc4 | ||
|
|
8f7ae2665c | ||
|
|
10fdf819d3 | ||
|
|
02928601ef | ||
|
|
c227f6dc2c | ||
|
|
673f0224c9 | ||
|
|
79c602f59c | ||
|
|
07c070e253 | ||
|
|
9bda3bd477 | ||
|
|
2c9ad9562e | ||
|
|
c264ee22e7 | ||
|
|
f194a689cc | ||
|
|
a36b350954 | ||
|
|
db4278fb9d | ||
|
|
39ba4cff2f | ||
|
|
d68da74790 | ||
|
|
5fc45cd736 | ||
|
|
5ae2f3d081 | ||
|
|
478bf643bf | ||
|
|
7929895b11 | ||
|
|
da11a72b4c | ||
|
|
1649368cee | ||
|
|
a528d62c16 | ||
|
|
bd13dbdad0 | ||
|
|
8e7ffd9e16 | ||
|
|
f0bff09b5e | ||
|
|
0e959b3019 | ||
|
|
983cd9c3fc | ||
|
|
2236ca3e12 | ||
|
|
f3afa6a7d9 | ||
|
|
ce7e2e3243 | ||
|
|
13416825b1 | ||
|
|
6c664e7ba9 | ||
|
|
34359617b5 | ||
|
|
9e2696b9bc | ||
|
|
bf840e8bfa | ||
|
|
1f03c140f5 | ||
|
|
2de161ce0e | ||
|
|
1171106afb | ||
|
|
f57ae73071 | ||
|
|
59872b5698 | ||
|
|
7cd8ea00d1 | ||
|
|
4b2f38926a | ||
|
|
537c95cf29 | ||
|
|
81a5722708 | ||
|
|
c150b913ac | ||
|
|
3e4b67db6c | ||
|
|
d727f8ff50 | ||
|
|
9546bf1dee | ||
|
|
dd9ce34d18 | ||
|
|
73f2d972e4 | ||
|
|
7d699c6c35 | ||
|
|
21f23f67f4 | ||
|
|
8874ba2779 | ||
|
|
420538e6e7 | ||
|
|
8eb68b54d9 | ||
|
|
80202f33cb | ||
|
|
c24579bfb2 | ||
|
|
21256c4529 | ||
|
|
668626b920 | ||
|
|
cbfa3bb56d | ||
|
|
536fcf02d7 | ||
|
|
a8ac3acbbe | ||
|
|
7980155375 | ||
|
|
aa855e31c8 | ||
|
|
675ee8e813 | ||
|
|
50ccce7387 | ||
|
|
40b561ea69 | ||
|
|
a0f73bd30f | ||
|
|
1b7fcce42d | ||
|
|
4749af6e90 | ||
|
|
f7ad40263b | ||
|
|
e5b25bfa58 | ||
|
|
1d23adcda3 | ||
|
|
0216d36ab7 | ||
|
|
2bec20ad76 | ||
|
|
93c1245b0f | ||
|
|
72504d7619 | ||
|
|
320aa34d39 | ||
|
|
87f2a4242e | ||
|
|
9bf0cbd659 | ||
|
|
b1470fd9b8 | ||
|
|
08016dc3b6 | ||
|
|
7a448f5528 | ||
|
|
4ac23bf14c | ||
|
|
bc708dee30 | ||
|
|
2888e5748e | ||
|
|
88f0a33e69 | ||
|
|
3165f92b6b | ||
|
|
3bd0fca633 | ||
|
|
cdff10d281 | ||
|
|
e425741c34 | ||
|
|
20a367b243 | ||
|
|
fdded9e7ee | ||
|
|
7d29bff136 | ||
|
|
0abfbeed3c | ||
|
|
35b7c3038a | ||
|
|
46dd96a4b7 | ||
|
|
788232ca35 | ||
|
|
3b458738e0 | ||
|
|
2c8fc67ab1 | ||
|
|
9b3ed3ed72 | ||
|
|
c59197e87a | ||
|
|
03e3c88d8b | ||
|
|
39693786ef | ||
|
|
357c324df1 | ||
|
|
650482208c | ||
|
|
2acad4a78c | ||
|
|
65ee4e1916 | ||
|
|
275bbc81f0 | ||
|
|
beafcf74ab | ||
|
|
e47909bb3e | ||
|
|
0b3b9c2257 | ||
|
|
8fb7a7e4cd | ||
|
|
c5ed148c52 | ||
|
|
e774c710a8 | ||
|
|
d237180a98 | ||
|
|
d8b618f7c3 | ||
|
|
e888a95bd1 | ||
|
|
36c2404a46 | ||
|
|
ba673beb82 | ||
|
|
4b56701152 | ||
|
|
59227116f3 | ||
|
|
9b0975b2ac | ||
|
|
3a39a5caa3 | ||
|
|
93e270f379 | ||
|
|
98c81fa2af | ||
|
|
1bb32a05a9 | ||
|
|
5dd4b77270 | ||
|
|
737d1aac7c | ||
|
|
886feae4ca | ||
|
|
1dfe26f14f | ||
|
|
d66fcd23df | ||
|
|
bdfb47e999 | ||
|
|
10300cc478 | ||
|
|
ababa639b3 | ||
|
|
9f6569d658 | ||
|
|
24c22ebdc7 | ||
|
|
6c365fffde | ||
|
|
dbb80dd6c0 | ||
|
|
624834de9c | ||
|
|
d31995f878 | ||
|
|
017b1cae26 | ||
|
|
c09f15b0e9 | ||
|
|
68284bed74 | ||
|
|
9a44d668d6 | ||
|
|
67e0197a7a | ||
|
|
a5a8cfa17d | ||
|
|
60c3e701e9 | ||
|
|
b9b129dcf5 | ||
|
|
d882ab236a | ||
|
|
140cc0e486 | ||
|
|
6ac7c0f893 | ||
|
|
096d50617f | ||
|
|
9dd8c0cc4f | ||
|
|
de0fab86ec | ||
|
|
bb36dd3893 | ||
|
|
ada837ee95 | ||
|
|
67e73173f6 | ||
|
|
4b63829eef | ||
|
|
029411d3fa | ||
|
|
6ba033f934 | ||
|
|
3734fa948f | ||
|
|
336742e335 | ||
|
|
66ca424d3a | ||
|
|
2da0a91a36 | ||
|
|
fee1bde231 | ||
|
|
4a94430bf0 | ||
|
|
cc337f7b1e | ||
|
|
d8a06777fe | ||
|
|
9207eedbfb | ||
|
|
c97b832648 | ||
|
|
4ef629f79d | ||
|
|
0b4e3c3db5 | ||
|
|
f12cc523b4 | ||
|
|
5c3c9d2ed1 | ||
|
|
3ac3673326 | ||
|
|
1a3940575e | ||
|
|
16c8b1efab | ||
|
|
0e789be09f | ||
|
|
a948c7d69d | ||
|
|
d8ec0103a9 | ||
|
|
50161670ce | ||
|
|
c1f612dce1 | ||
|
|
6fb74482d7 | ||
|
|
4b680ffa5f | ||
|
|
c71c8d56ce | ||
|
|
295ae7b4bc | ||
|
|
839c884cef | ||
|
|
13ffe7acfb | ||
|
|
39a0c0d96e | ||
|
|
a95a542148 | ||
|
|
b3cb2ac3ee | ||
|
|
759fe54132 | ||
|
|
519a888e82 | ||
|
|
4f1e4e7471 | ||
|
|
7b8a32f630 | ||
|
|
92d91a65bb | ||
|
|
dab5289177 | ||
|
|
a77cb1e579 | ||
|
|
01bdda0ae6 | ||
|
|
fbe35e6e6b | ||
|
|
a3cd74e30b | ||
|
|
dbd4781de1 | ||
|
|
6d48316436 | ||
|
|
cca6965cd1 | ||
|
|
dd63ed7e69 | ||
|
|
61e2283146 | ||
|
|
97eb768748 | ||
|
|
be8b5a8aeb | ||
|
|
99ed39b26c | ||
|
|
48a0eb90a7 | ||
|
|
3c342077d6 | ||
|
|
f1bef1e7e6 | ||
|
|
da9749ecce | ||
|
|
fa7be597d2 | ||
|
|
53da418d68 | ||
|
|
897ed7e381 | ||
|
|
daf0939f09 | ||
|
|
7b1d6ddcf6 | ||
|
|
267e1dd0f8 | ||
|
|
c9d0bfce54 | ||
|
|
7f9e5e29a8 | ||
|
|
d0f685183d | ||
|
|
bed77bd356 | ||
|
|
bc0e3b254b | ||
|
|
47bf0ebb47 | ||
|
|
0acb95bbd5 | ||
|
|
8665f4a251 | ||
|
|
3adacb8799 | ||
|
|
76aa69b9ac | ||
|
|
78116f1596 | ||
|
|
36693b7d9d | ||
|
|
8ce68f93ea | ||
|
|
3512cb9599 | ||
|
|
ea164a2030 | ||
|
|
929ba70ef8 | ||
|
|
5b2113c43d | ||
|
|
6df2c0bab5 | ||
|
|
1c5193aa4d | ||
|
|
bd55fe868d | ||
|
|
87a2465a25 | ||
|
|
5f839ad3ee | ||
|
|
1663d8dfa9 | ||
|
|
08eafc54e6 | ||
|
|
fe1d8b137e | ||
|
|
39c0826f3c | ||
|
|
bf63b0993d | ||
|
|
f91a1363cb | ||
|
|
a2c9aa7662 | ||
|
|
d135da6c1d | ||
|
|
d27051f04d | ||
|
|
b28fa2a1ad | ||
|
|
77a91f5a8f | ||
|
|
dcc7ee98b3 | ||
|
|
30edb2a44f | ||
|
|
f63332a7aa | ||
|
|
86c37ce192 | ||
|
|
93e6c9e5a0 | ||
|
|
92e1fa4d3a | ||
|
|
bf7d292884 | ||
|
|
add8db0186 | ||
|
|
3e62c6ae2f | ||
|
|
cd4aa8ccd6 | ||
|
|
937dbdc71f | ||
|
|
66a7b508b2 | ||
|
|
a5493f7947 | ||
|
|
979c4907da | ||
|
|
b8f6fdeb2b | ||
|
|
067376cb3b | ||
|
|
bdbe9255a6 | ||
|
|
c460e1bbbe | ||
|
|
7e2b72fa5e | ||
|
|
6ee6a8a74f | ||
|
|
80984c94a1 | ||
|
|
1757b66467 | ||
|
|
8aa25af014 | ||
|
|
5a0e47be48 | ||
|
|
756a866ffd | ||
|
|
29305be23b | ||
|
|
8253cfd21d | ||
|
|
165a00896e | ||
|
|
2149ea1306 | ||
|
|
90547da007 | ||
|
|
9ec4881d8d | ||
|
|
487593af38 | ||
|
|
4e8f878d83 | ||
|
|
af6544c64d | ||
|
|
09e1f53b3e | ||
|
|
1c4f191f42 | ||
|
|
a37bd824d5 | ||
|
|
2c79173d20 | ||
|
|
eb45b89557 | ||
|
|
bf8c345341 | ||
|
|
ef46280716 | ||
|
|
2453e1284f | ||
|
|
95bcb272e0 | ||
|
|
e0e61b5262 | ||
|
|
3ddef56167 | ||
|
|
f8e6fb81d6 | ||
|
|
683ec87adf | ||
|
|
23edbe5ce7 | ||
|
|
6ff32a51e3 | ||
|
|
4cbac3a864 | ||
|
|
94a99b5bec | ||
|
|
810bf06e16 | ||
|
|
1254667b2c | ||
|
|
053eb8a0fd | ||
|
|
82ef380256 | ||
|
|
44449d8e72 | ||
|
|
6c3a0890c7 | ||
|
|
8c0def7c79 | ||
|
|
de77751779 | ||
|
|
cdf809926b | ||
|
|
d40341f1ad | ||
|
|
4a94fb91d7 | ||
|
|
24ea9ca947 | ||
|
|
98eb9bf2bd | ||
|
|
1eb30cf3ab | ||
|
|
6fd7c0ff8e | ||
|
|
263e81cb2c | ||
|
|
92ebf37d86 | ||
|
|
a10e406131 | ||
|
|
21095e80a7 | ||
|
|
55ae43ed03 | ||
|
|
9cc934a972 | ||
|
|
cdfec7ebb4 | ||
|
|
59ad69b637 | ||
|
|
ca6b759607 | ||
|
|
f9d857211f | ||
|
|
01ad8661d6 | ||
|
|
d21b8166f0 | ||
|
|
63582bb489 | ||
|
|
c19f2de3a8 | ||
|
|
d2e7b61eb2 | ||
|
|
13a448ebfe | ||
|
|
bad2e1f9c4 | ||
|
|
8edac51401 | ||
|
|
f34ba9bf96 | ||
|
|
82aea946a2 | ||
|
|
a0665dc431 | ||
|
|
e32d6cdecd | ||
|
|
23b43319a8 | ||
|
|
e7a7a18c43 | ||
|
|
8e5abcf5c2 | ||
|
|
e08e8641cb | ||
|
|
3e8f3cfb49 | ||
|
|
1eaaa5c6d3 | ||
|
|
1cc776d332 | ||
|
|
4009ae7d77 | ||
|
|
07506faa3a | ||
|
|
4d787ec93c | ||
|
|
188413a531 | ||
|
|
ad55c9cc19 | ||
|
|
9b3ac49298 | ||
|
|
4306b0caba | ||
|
|
ebd1baa42c | ||
|
|
6861bbed79 | ||
|
|
25f66e6ac0 | ||
|
|
838519e89f | ||
|
|
be4641b8f3 | ||
|
|
e861cab727 | ||
|
|
f8f87ec091 | ||
|
|
c0f1996478 | ||
|
|
106746ce58 | ||
|
|
62773fa88a | ||
|
|
28a8ed62f3 | ||
|
|
110751e992 | ||
|
|
0d447c9d50 | ||
|
|
827d6d1d2d | ||
|
|
a64972fe38 | ||
|
|
09bdc81aeb | ||
|
|
c057de3a3c | ||
|
|
1c4aff3ee1 |
@@ -58,7 +58,13 @@
|
||||
],
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
}
|
||||
},
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["homeassistant/components/*/manifest.json"],
|
||||
"url": "./script/json_schemas/manifest_schema.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,2 +1 @@
|
||||
custom: https://www.nabucasa.com
|
||||
github: balloob
|
||||
custom: https://www.openhomefoundation.org
|
||||
|
||||
20
.github/workflows/builder.yml
vendored
20
.github/workflows/builder.yml
vendored
@@ -27,12 +27,12 @@ jobs:
|
||||
publish: ${{ steps.version.outputs.publish }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -242,7 +242,7 @@ jobs:
|
||||
- green
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set build additional args
|
||||
run: |
|
||||
@@ -279,7 +279,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.7.0
|
||||
@@ -451,10 +451,10 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -499,7 +499,7 @@ jobs:
|
||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
|
||||
140
.github/workflows/ci.yaml
vendored
140
.github/workflows/ci.yaml
vendored
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 11
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2024.11"
|
||||
HA_SHORT_VERSION: "2024.12"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Generate partial Python venv restore key
|
||||
id: generate_python_cache_key
|
||||
run: |
|
||||
@@ -231,16 +231,16 @@ jobs:
|
||||
- info
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.1.1
|
||||
uses: actions/cache@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.1.1
|
||||
uses: actions/cache@v4.1.2
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
@@ -277,16 +277,16 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -317,16 +317,16 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -335,7 +335,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -357,16 +357,16 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -375,7 +375,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -447,7 +447,7 @@ jobs:
|
||||
- script/hassfest/docker/Dockerfile
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Register hadolint problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||
@@ -466,10 +466,10 @@ jobs:
|
||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -482,7 +482,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.1.1
|
||||
uses: actions/cache@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
lookup-only: true
|
||||
@@ -491,7 +491,7 @@ jobs:
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4.1.1
|
||||
uses: actions/cache@v4.1.2
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
@@ -550,16 +550,16 @@ jobs:
|
||||
sudo apt-get -y install \
|
||||
libturbojpeg
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -583,16 +583,16 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -615,37 +615,41 @@ jobs:
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
|| github.event.inputs.audit-licenses-only == 'true')
|
||||
&& needs.info.outputs.requirements == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Run pip-licenses
|
||||
- name: Extract license data
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pip-licenses --format=json --output-file=licenses.json
|
||||
python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json
|
||||
- name: Upload licenses
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
with:
|
||||
name: licenses
|
||||
path: licenses.json
|
||||
- name: Process licenses
|
||||
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
||||
path: licenses-${{ matrix.python-version }}.json
|
||||
- name: Check licenses
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python -m script.licenses licenses.json
|
||||
python -m script.licenses check licenses-${{ matrix.python-version }}.json
|
||||
|
||||
pylint:
|
||||
name: Check pylint
|
||||
@@ -660,16 +664,16 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -707,16 +711,16 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -752,10 +756,10 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -768,7 +772,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -776,7 +780,7 @@ jobs:
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@v4.1.1
|
||||
uses: actions/cache@v4.1.2
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
@@ -831,16 +835,16 @@ jobs:
|
||||
libturbojpeg \
|
||||
libgammu-dev
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -895,16 +899,16 @@ jobs:
|
||||
libturbojpeg \
|
||||
libgammu-dev
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1015,16 +1019,16 @@ jobs:
|
||||
libturbojpeg \
|
||||
libmariadb-dev-compat
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1098,7 +1102,7 @@ jobs:
|
||||
./script/check_dirty
|
||||
|
||||
pytest-postgres:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
services:
|
||||
postgres:
|
||||
image: ${{ matrix.postgresql-group }}
|
||||
@@ -1138,19 +1142,21 @@ jobs:
|
||||
sudo apt-get -y install \
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libturbojpeg
|
||||
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
|
||||
sudo apt-get -y install \
|
||||
postgresql-server-dev-14
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1236,7 +1242,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
@@ -1287,16 +1293,16 @@ jobs:
|
||||
libturbojpeg \
|
||||
libgammu-dev
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.1
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1374,7 +1380,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.26.13
|
||||
uses: github/codeql-action/init@v3.27.0
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.26.13
|
||||
uses: github/codeql-action/analyze@v3.27.0
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@@ -19,10 +19,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
||||
8
.github/workflows/wheels.yml
vendored
8
.github/workflows/wheels.yml
vendored
@@ -32,11 +32,11 @@ jobs:
|
||||
architectures: ${{ steps.info.outputs.architectures }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.2.0
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.7.0
|
||||
rev: v0.7.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
||||
@@ -124,6 +124,7 @@ homeassistant.components.bryant_evolution.*
|
||||
homeassistant.components.bthome.*
|
||||
homeassistant.components.button.*
|
||||
homeassistant.components.calendar.*
|
||||
homeassistant.components.cambridge_audio.*
|
||||
homeassistant.components.camera.*
|
||||
homeassistant.components.canary.*
|
||||
homeassistant.components.cert_expiry.*
|
||||
@@ -208,6 +209,7 @@ homeassistant.components.geo_location.*
|
||||
homeassistant.components.geocaching.*
|
||||
homeassistant.components.gios.*
|
||||
homeassistant.components.glances.*
|
||||
homeassistant.components.go2rtc.*
|
||||
homeassistant.components.goalzero.*
|
||||
homeassistant.components.google.*
|
||||
homeassistant.components.google_assistant_sdk.*
|
||||
@@ -322,6 +324,7 @@ homeassistant.components.moon.*
|
||||
homeassistant.components.mopeka.*
|
||||
homeassistant.components.motionmount.*
|
||||
homeassistant.components.mqtt.*
|
||||
homeassistant.components.music_assistant.*
|
||||
homeassistant.components.my.*
|
||||
homeassistant.components.mysensors.*
|
||||
homeassistant.components.myuplink.*
|
||||
|
||||
10
.vscode/settings.default.json
vendored
10
.vscode/settings.default.json
vendored
@@ -6,5 +6,13 @@
|
||||
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
|
||||
"python.testing.pytestEnabled": false,
|
||||
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
||||
"pylint.importStrategy": "fromEnvironment"
|
||||
"pylint.importStrategy": "fromEnvironment",
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [
|
||||
"homeassistant/components/*/manifest.json"
|
||||
],
|
||||
"url": "./script/json_schemas/manifest_schema.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
22
CODEOWNERS
22
CODEOWNERS
@@ -617,8 +617,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/hlk_sw16/ @jameshilliard
|
||||
/homeassistant/components/holiday/ @jrieger @gjohansson-ST
|
||||
/tests/components/holiday/ @jrieger @gjohansson-ST
|
||||
/homeassistant/components/home_connect/ @DavidMStraub
|
||||
/tests/components/home_connect/ @DavidMStraub
|
||||
/homeassistant/components/home_connect/ @DavidMStraub @Diegorro98
|
||||
/tests/components/home_connect/ @DavidMStraub @Diegorro98
|
||||
/homeassistant/components/homeassistant/ @home-assistant/core
|
||||
/tests/components/homeassistant/ @home-assistant/core
|
||||
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
|
||||
@@ -659,6 +659,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||
/homeassistant/components/husqvarna_automower/ @Thomas55555
|
||||
/tests/components/husqvarna_automower/ @Thomas55555
|
||||
/homeassistant/components/husqvarna_automower_ble/ @alistair23
|
||||
/tests/components/husqvarna_automower_ble/ @alistair23
|
||||
/homeassistant/components/huum/ @frwickst
|
||||
/tests/components/huum/ @frwickst
|
||||
/homeassistant/components/hvv_departures/ @vigonotion
|
||||
@@ -819,6 +821,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/lektrico/ @lektrico
|
||||
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
||||
/homeassistant/components/lidarr/ @tkdrob
|
||||
/tests/components/lidarr/ @tkdrob
|
||||
/homeassistant/components/lifx/ @Djelibeybi
|
||||
@@ -950,6 +954,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/msteams/ @peroyvind
|
||||
/homeassistant/components/mullvad/ @meichthys
|
||||
/tests/components/mullvad/ @meichthys
|
||||
/homeassistant/components/music_assistant/ @music-assistant
|
||||
/tests/components/music_assistant/ @music-assistant
|
||||
/homeassistant/components/mutesync/ @currentoor
|
||||
/tests/components/mutesync/ @currentoor
|
||||
/homeassistant/components/my/ @home-assistant/core
|
||||
@@ -1047,6 +1053,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/onewire/ @garbled1 @epenet
|
||||
/tests/components/onewire/ @garbled1 @epenet
|
||||
/homeassistant/components/onkyo/ @arturpragacz
|
||||
/tests/components/onkyo/ @arturpragacz
|
||||
/homeassistant/components/onvif/ @hunterjm
|
||||
/tests/components/onvif/ @hunterjm
|
||||
/homeassistant/components/open_meteo/ @frenck
|
||||
@@ -1088,6 +1095,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ovo_energy/ @timmo001
|
||||
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
||||
/tests/components/p1_monitor/ @klaasnicolaas
|
||||
/homeassistant/components/palazzetti/ @dotvav
|
||||
/tests/components/palazzetti/ @dotvav
|
||||
/homeassistant/components/panel_custom/ @home-assistant/frontend
|
||||
/tests/components/panel_custom/ @home-assistant/frontend
|
||||
/homeassistant/components/peco/ @IceBotYT
|
||||
@@ -1237,8 +1246,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/roku/ @ctalkington
|
||||
/homeassistant/components/romy/ @xeniter
|
||||
/tests/components/romy/ @xeniter
|
||||
/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn @Xitee1 @Orhideous
|
||||
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Xitee1 @Orhideous
|
||||
/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
||||
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
||||
/homeassistant/components/roon/ @pavoni
|
||||
/tests/components/roon/ @pavoni
|
||||
/homeassistant/components/rpi_power/ @shenxn @swetoast
|
||||
@@ -1349,6 +1358,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/smarttub/ @mdz
|
||||
/tests/components/smarttub/ @mdz
|
||||
/homeassistant/components/smarty/ @z0mbieprocess
|
||||
/tests/components/smarty/ @z0mbieprocess
|
||||
/homeassistant/components/smhi/ @gjohansson-ST
|
||||
/tests/components/smhi/ @gjohansson-ST
|
||||
/homeassistant/components/smlight/ @tl-sl
|
||||
@@ -1412,8 +1422,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/stt/ @home-assistant/core
|
||||
/homeassistant/components/subaru/ @G-Two
|
||||
/tests/components/subaru/ @G-Two
|
||||
/homeassistant/components/suez_water/ @ooii
|
||||
/tests/components/suez_water/ @ooii
|
||||
/homeassistant/components/suez_water/ @ooii @jb101010-2
|
||||
/tests/components/suez_water/ @ooii @jb101010-2
|
||||
/homeassistant/components/sun/ @Swamp-Ig
|
||||
/tests/components/sun/ @Swamp-Ig
|
||||
/homeassistant/components/sunweg/ @rokam
|
||||
|
||||
@@ -12,7 +12,7 @@ ENV \
|
||||
ARG QEMU_CPU
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.4.22
|
||||
RUN pip3 install uv==0.4.28
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
@@ -54,7 +54,7 @@ RUN \
|
||||
"armv7") go2rtc_suffix='arm' ;; \
|
||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||
esac \
|
||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.4/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.6/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||
&& chmod +x /bin/go2rtc \
|
||||
# Verify go2rtc can be executed
|
||||
&& go2rtc --version
|
||||
|
||||
@@ -9,6 +9,7 @@ import os
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from .backup_restore import restore_backup
|
||||
from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||
|
||||
FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
||||
@@ -182,6 +183,9 @@ def main() -> int:
|
||||
return scripts.run(args.script)
|
||||
|
||||
config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config))
|
||||
if restore_backup(config_dir):
|
||||
return RESTART_EXIT_CODE
|
||||
|
||||
ensure_config_path(config_dir)
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
|
||||
126
homeassistant/backup_restore.py
Normal file
126
homeassistant/backup_restore.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""Home Assistant module to handle restoring backups."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
import securetar
|
||||
|
||||
from .const import __version__ as HA_VERSION
|
||||
|
||||
RESTORE_BACKUP_FILE = ".HA_RESTORE"
|
||||
KEEP_PATHS = ("backups",)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RestoreBackupFileContent:
|
||||
"""Definition for restore backup file content."""
|
||||
|
||||
backup_file_path: Path
|
||||
|
||||
|
||||
def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent | None:
|
||||
"""Return the contents of the restore backup file."""
|
||||
instruction_path = config_dir.joinpath(RESTORE_BACKUP_FILE)
|
||||
try:
|
||||
instruction_content = instruction_path.read_text(encoding="utf-8")
|
||||
return RestoreBackupFileContent(
|
||||
backup_file_path=Path(instruction_content.split(";")[0])
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def _clear_configuration_directory(config_dir: Path) -> None:
|
||||
"""Delete all files and directories in the config directory except for the backups directory."""
|
||||
keep_paths = [config_dir.joinpath(path) for path in KEEP_PATHS]
|
||||
config_contents = sorted(
|
||||
[entry for entry in config_dir.iterdir() if entry not in keep_paths]
|
||||
)
|
||||
|
||||
for entry in config_contents:
|
||||
entrypath = config_dir.joinpath(entry)
|
||||
|
||||
if entrypath.is_file():
|
||||
entrypath.unlink()
|
||||
elif entrypath.is_dir():
|
||||
shutil.rmtree(entrypath)
|
||||
|
||||
|
||||
def _extract_backup(config_dir: Path, backup_file_path: Path) -> None:
|
||||
"""Extract the backup file to the config directory."""
|
||||
with (
|
||||
TemporaryDirectory() as tempdir,
|
||||
securetar.SecureTarFile(
|
||||
backup_file_path,
|
||||
gzip=False,
|
||||
mode="r",
|
||||
) as ostf,
|
||||
):
|
||||
ostf.extractall(
|
||||
path=Path(tempdir, "extracted"),
|
||||
members=securetar.secure_path(ostf),
|
||||
filter="fully_trusted",
|
||||
)
|
||||
backup_meta_file = Path(tempdir, "extracted", "backup.json")
|
||||
backup_meta = json.loads(backup_meta_file.read_text(encoding="utf8"))
|
||||
|
||||
if (
|
||||
backup_meta_version := AwesomeVersion(
|
||||
backup_meta["homeassistant"]["version"]
|
||||
)
|
||||
) > HA_VERSION:
|
||||
raise ValueError(
|
||||
f"You need at least Home Assistant version {backup_meta_version} to restore this backup"
|
||||
)
|
||||
|
||||
with securetar.SecureTarFile(
|
||||
Path(
|
||||
tempdir,
|
||||
"extracted",
|
||||
f"homeassistant.tar{'.gz' if backup_meta["compressed"] else ''}",
|
||||
),
|
||||
gzip=backup_meta["compressed"],
|
||||
mode="r",
|
||||
) as istf:
|
||||
for member in istf.getmembers():
|
||||
if member.name == "data":
|
||||
continue
|
||||
member.name = member.name.replace("data/", "")
|
||||
_clear_configuration_directory(config_dir)
|
||||
istf.extractall(
|
||||
path=config_dir,
|
||||
members=[
|
||||
member
|
||||
for member in securetar.secure_path(istf)
|
||||
if member.name != "data"
|
||||
],
|
||||
filter="fully_trusted",
|
||||
)
|
||||
|
||||
|
||||
def restore_backup(config_dir_path: str) -> bool:
|
||||
"""Restore the backup file if any.
|
||||
|
||||
Returns True if a restore backup file was found and restored, False otherwise.
|
||||
"""
|
||||
config_dir = Path(config_dir_path)
|
||||
if not (restore_content := restore_backup_file_content(config_dir)):
|
||||
return False
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
||||
backup_file_path = restore_content.backup_file_path
|
||||
_LOGGER.info("Restoring %s", backup_file_path)
|
||||
try:
|
||||
_extract_backup(config_dir, backup_file_path)
|
||||
except FileNotFoundError as err:
|
||||
raise ValueError(f"Backup file {backup_file_path} does not exist") from err
|
||||
_LOGGER.info("Restore complete, restarting")
|
||||
return True
|
||||
@@ -70,6 +70,7 @@ from .const import (
|
||||
REQUIRED_NEXT_PYTHON_VER,
|
||||
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
||||
)
|
||||
from .core_config import async_process_ha_core_config
|
||||
from .exceptions import HomeAssistantError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
@@ -479,7 +480,7 @@ async def async_from_config_dict(
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
await conf_util.async_process_ha_core_config(hass, core_config)
|
||||
await async_process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as config_err:
|
||||
conf_util.async_log_schema_error(config_err, core.DOMAIN, core_config, hass)
|
||||
async_notify_setup_error(hass, core.DOMAIN)
|
||||
|
||||
5
homeassistant/brands/husqvarna.json
Normal file
5
homeassistant/brands/husqvarna.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "husqvarna",
|
||||
"name": "Husqvarna",
|
||||
"integrations": ["husqvarna_automower", "husqvarna_automower_ble"]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "lg",
|
||||
"name": "LG",
|
||||
"integrations": ["lg_netcast", "lg_soundbar", "webostv"]
|
||||
"integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
|
||||
}
|
||||
|
||||
@@ -7,13 +7,9 @@ from jaraco.abode.devices.alarm import Alarm
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
@@ -44,14 +40,14 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanelEntity):
|
||||
_device: Alarm
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
def alarm_state(self) -> AlarmControlPanelState | None:
|
||||
"""Return the state of the device."""
|
||||
if self._device.is_standby:
|
||||
return STATE_ALARM_DISARMED
|
||||
return AlarmControlPanelState.DISARMED
|
||||
if self._device.is_away:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return AlarmControlPanelState.ARMED_AWAY
|
||||
if self._device.is_home:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
return AlarmControlPanelState.ARMED_HOME
|
||||
return None
|
||||
|
||||
def alarm_disarm(self, code: str | None = None) -> None:
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import Any
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.hassio import HassioServiceInfo
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@@ -18,6 +17,7 @@ from homeassistant.const import (
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ async def async_setup_entry(
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name="Advantage Air",
|
||||
update_method=async_get,
|
||||
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
|
||||
|
||||
@@ -5,12 +5,7 @@ from __future__ import annotations
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
AlarmControlPanelEntityFeature,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED,
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -65,37 +60,37 @@ class AgentBaseStation(AlarmControlPanelEntity):
|
||||
self._attr_available = self._client.is_available
|
||||
armed = self._client.is_armed
|
||||
if armed is None:
|
||||
self._attr_state = None
|
||||
self._attr_alarm_state = None
|
||||
return
|
||||
if armed:
|
||||
prof = (await self._client.get_active_profile()).lower()
|
||||
self._attr_state = STATE_ALARM_ARMED_AWAY
|
||||
self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
|
||||
if prof == CONF_HOME_MODE_NAME:
|
||||
self._attr_state = STATE_ALARM_ARMED_HOME
|
||||
self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
|
||||
elif prof == CONF_NIGHT_MODE_NAME:
|
||||
self._attr_state = STATE_ALARM_ARMED_NIGHT
|
||||
self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
|
||||
else:
|
||||
self._attr_state = STATE_ALARM_DISARMED
|
||||
self._attr_alarm_state = AlarmControlPanelState.DISARMED
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
await self._client.disarm()
|
||||
self._attr_state = STATE_ALARM_DISARMED
|
||||
self._attr_alarm_state = AlarmControlPanelState.DISARMED
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command. Uses custom mode."""
|
||||
await self._client.arm()
|
||||
await self._client.set_active_profile(CONF_AWAY_MODE_NAME)
|
||||
self._attr_state = STATE_ALARM_ARMED_AWAY
|
||||
self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command. Uses custom mode."""
|
||||
await self._client.arm()
|
||||
await self._client.set_active_profile(CONF_HOME_MODE_NAME)
|
||||
self._attr_state = STATE_ALARM_ARMED_HOME
|
||||
self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
|
||||
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command. Uses custom mode."""
|
||||
await self._client.arm()
|
||||
await self._client.set_active_profile(CONF_NIGHT_MODE_NAME)
|
||||
self._attr_state = STATE_ALARM_ARMED_NIGHT
|
||||
self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Config flow for AirNow integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -12,7 +14,6 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -120,12 +121,12 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlow:
|
||||
) -> AirNowOptionsFlowHandler:
|
||||
"""Return the options flow."""
|
||||
return AirNowOptionsFlowHandler(config_entry)
|
||||
return AirNowOptionsFlowHandler()
|
||||
|
||||
|
||||
class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class AirNowOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle an options flow for AirNow."""
|
||||
|
||||
async def async_step_init(
|
||||
@@ -136,12 +137,7 @@ class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
return self.async_create_entry(data=user_input)
|
||||
|
||||
options_schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_RADIUS): vol.All(
|
||||
int,
|
||||
vol.Range(min=5),
|
||||
),
|
||||
}
|
||||
{vol.Optional(CONF_RADIUS): vol.All(int, vol.Range(min=5))}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -42,6 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_method=_update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
|
||||
@@ -2,75 +2,27 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
|
||||
from bleak_retry_connector import close_stale_connections_by_address
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MAX_RETRIES_AFTER_STARTUP
|
||||
from .const import MAX_RETRIES_AFTER_STARTUP
|
||||
from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator[AirthingsDevice]
|
||||
AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AirthingsBLEConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Airthings BLE device from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
address = entry.unique_id
|
||||
|
||||
is_metric = hass.config.units is METRIC_SYSTEM
|
||||
assert address is not None
|
||||
|
||||
await close_stale_connections_by_address(address)
|
||||
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, address)
|
||||
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not find Airthings device with address {address}"
|
||||
)
|
||||
|
||||
airthings = AirthingsBluetoothDeviceData(_LOGGER, is_metric)
|
||||
|
||||
async def _async_update_method() -> AirthingsDevice:
|
||||
"""Get data from Airthings BLE."""
|
||||
try:
|
||||
data = await airthings.update_device(ble_device)
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
|
||||
return data
|
||||
|
||||
coordinator: AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_method=_async_update_method,
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
|
||||
coordinator = AirthingsBLEDataUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Once its setup and we know we are not going to delay
|
||||
# the startup of Home Assistant, we can set the max attempts
|
||||
# to a higher value. If the first connection attempt fails,
|
||||
# Home Assistant's built-in retry logic will take over.
|
||||
airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP)
|
||||
coordinator.airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP)
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
|
||||
68
homeassistant/components/airthings_ble/coordinator.py
Normal file
68
homeassistant/components/airthings_ble/coordinator.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""The Airthings BLE integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak_retry_connector import close_stale_connections_by_address
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
|
||||
"""Class to manage fetching Airthings BLE data."""
|
||||
|
||||
ble_device: BLEDevice
|
||||
config_entry: AirthingsBLEConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: AirthingsBLEConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.airthings = AirthingsBluetoothDeviceData(
|
||||
_LOGGER, hass.config.units is METRIC_SYSTEM
|
||||
)
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
address = self.config_entry.unique_id
|
||||
|
||||
assert address is not None
|
||||
|
||||
await close_stale_connections_by_address(address)
|
||||
|
||||
ble_device = bluetooth.async_ble_device_from_address(self.hass, address)
|
||||
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not find Airthings device with address {address}"
|
||||
)
|
||||
self.ble_device = ble_device
|
||||
|
||||
async def _async_update_data(self) -> AirthingsDevice:
|
||||
"""Get data from Airthings BLE."""
|
||||
try:
|
||||
data = await self.airthings.update_device(self.ble_device)
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
|
||||
return data
|
||||
@@ -34,8 +34,8 @@ from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from . import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
|
||||
from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE
|
||||
from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER]
|
||||
|
||||
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
|
||||
@@ -19,8 +17,6 @@ type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: Airtouch5ConfigEntry) -> bool:
|
||||
"""Set up Airtouch 5 from a config entry."""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
# Create API instance
|
||||
host = entry.data[CONF_HOST]
|
||||
client = Airtouch5SimpleClient(host)
|
||||
|
||||
@@ -204,6 +204,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
LOGGER,
|
||||
config_entry=entry,
|
||||
name=async_get_geography_id(entry.data),
|
||||
# We give a placeholder update interval in order to create the coordinator;
|
||||
# then, below, we use the coordinator's presence (along with any other
|
||||
|
||||
@@ -81,6 +81,7 @@ async def async_setup_entry(
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
LOGGER,
|
||||
config_entry=entry,
|
||||
name="Node/Pro data",
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
update_method=async_get_data,
|
||||
|
||||
@@ -24,6 +24,7 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.CLIMATE,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.WATER_HEATER,
|
||||
]
|
||||
|
||||
|
||||
122
homeassistant/components/airzone/switch.py
Normal file
122
homeassistant/components/airzone/switch.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""Support for the Airzone switch."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Final
|
||||
|
||||
from aioairzone.const import API_ON, AZD_ON, AZD_ZONES
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AirzoneConfigEntry
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
from .entity import AirzoneEntity, AirzoneZoneEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AirzoneSwitchDescription(SwitchEntityDescription):
|
||||
"""Class to describe an Airzone switch entity."""
|
||||
|
||||
api_param: str
|
||||
|
||||
|
||||
ZONE_SWITCH_TYPES: Final[tuple[AirzoneSwitchDescription, ...]] = (
|
||||
AirzoneSwitchDescription(
|
||||
api_param=API_ON,
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
key=AZD_ON,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AirzoneConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Airzone switch from a config_entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
added_zones: set[str] = set()
|
||||
|
||||
def _async_entity_listener() -> None:
|
||||
"""Handle additions of switch."""
|
||||
|
||||
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||
received_zones = set(zones_data)
|
||||
new_zones = received_zones - added_zones
|
||||
if new_zones:
|
||||
async_add_entities(
|
||||
AirzoneZoneSwitch(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
system_zone_id,
|
||||
zones_data.get(system_zone_id),
|
||||
)
|
||||
for system_zone_id in new_zones
|
||||
for description in ZONE_SWITCH_TYPES
|
||||
if description.key in zones_data.get(system_zone_id)
|
||||
)
|
||||
added_zones.update(new_zones)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||
_async_entity_listener()
|
||||
|
||||
|
||||
class AirzoneBaseSwitch(AirzoneEntity, SwitchEntity):
|
||||
"""Define an Airzone switch."""
|
||||
|
||||
entity_description: AirzoneSwitchDescription
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Update attributes when the coordinator updates."""
|
||||
self._async_update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update switch attributes."""
|
||||
self._attr_is_on = self.get_airzone_value(self.entity_description.key)
|
||||
|
||||
|
||||
class AirzoneZoneSwitch(AirzoneZoneEntity, AirzoneBaseSwitch):
|
||||
"""Define an Airzone Zone switch."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: AirzoneSwitchDescription,
|
||||
entry: ConfigEntry,
|
||||
system_zone_id: str,
|
||||
zone_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, entry, system_zone_id, zone_data)
|
||||
|
||||
self._attr_name = None
|
||||
self._attr_unique_id = (
|
||||
f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
|
||||
)
|
||||
self.entity_description = description
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
param = self.entity_description.api_param
|
||||
await self._async_update_hvac_params({param: True})
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
param = self.entity_description.api_param
|
||||
await self._async_update_hvac_params({param: False})
|
||||
@@ -17,6 +17,7 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.CLIMATE,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.WATER_HEATER,
|
||||
]
|
||||
|
||||
|
||||
@@ -310,6 +310,10 @@ class AirzoneDeviceClimate(AirzoneClimate):
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
||||
if hvac_mode is not None:
|
||||
await self.async_set_hvac_mode(hvac_mode)
|
||||
|
||||
params: dict[str, Any] = {}
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
params[API_SETPOINT] = {
|
||||
@@ -333,9 +337,6 @@ class AirzoneDeviceClimate(AirzoneClimate):
|
||||
}
|
||||
await self._async_update_params(params)
|
||||
|
||||
if ATTR_HVAC_MODE in kwargs:
|
||||
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
|
||||
|
||||
|
||||
class AirzoneDeviceGroupClimate(AirzoneClimate):
|
||||
"""Define an Airzone Cloud DeviceGroup base class."""
|
||||
@@ -366,6 +367,10 @@ class AirzoneDeviceGroupClimate(AirzoneClimate):
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
||||
if hvac_mode is not None:
|
||||
await self.async_set_hvac_mode(hvac_mode)
|
||||
|
||||
params: dict[str, Any] = {}
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
params[API_PARAMS] = {
|
||||
@@ -376,9 +381,6 @@ class AirzoneDeviceGroupClimate(AirzoneClimate):
|
||||
}
|
||||
await self._async_update_params(params)
|
||||
|
||||
if ATTR_HVAC_MODE in kwargs:
|
||||
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set hvac mode."""
|
||||
params: dict[str, Any] = {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.6.7"]
|
||||
"requirements": ["aioairzone-cloud==0.6.10"]
|
||||
}
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Final
|
||||
|
||||
from aioairzone_cloud.common import AirQualityMode
|
||||
from aioairzone_cloud.common import AirQualityMode, OperationMode
|
||||
from aioairzone_cloud.const import (
|
||||
API_AQ_MODE_CONF,
|
||||
API_MODE,
|
||||
API_VALUE,
|
||||
AZD_AQ_MODE_CONF,
|
||||
AZD_MASTER,
|
||||
AZD_MODE,
|
||||
AZD_MODES,
|
||||
AZD_ZONES,
|
||||
)
|
||||
|
||||
@@ -28,7 +33,10 @@ class AirzoneSelectDescription(SelectEntityDescription):
|
||||
"""Class to describe an Airzone select entity."""
|
||||
|
||||
api_param: str
|
||||
options_dict: dict[str, str]
|
||||
options_dict: dict[str, Any]
|
||||
options_fn: Callable[[dict[str, Any], dict[str, Any]], list[str]] = (
|
||||
lambda zone_data, value: list(value)
|
||||
)
|
||||
|
||||
|
||||
AIR_QUALITY_MAP: Final[dict[str, str]] = {
|
||||
@@ -37,6 +45,35 @@ AIR_QUALITY_MAP: Final[dict[str, str]] = {
|
||||
"auto": AirQualityMode.AUTO,
|
||||
}
|
||||
|
||||
MODE_MAP: Final[dict[str, int]] = {
|
||||
"cool": OperationMode.COOLING,
|
||||
"dry": OperationMode.DRY,
|
||||
"fan": OperationMode.VENTILATION,
|
||||
"heat": OperationMode.HEATING,
|
||||
"heat_cool": OperationMode.AUTO,
|
||||
"stop": OperationMode.STOP,
|
||||
}
|
||||
|
||||
|
||||
def main_zone_options(
|
||||
zone_data: dict[str, Any],
|
||||
options: dict[str, int],
|
||||
) -> list[str]:
|
||||
"""Filter available modes."""
|
||||
modes = zone_data.get(AZD_MODES, [])
|
||||
return [k for k, v in options.items() if v in modes]
|
||||
|
||||
|
||||
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_MODE,
|
||||
key=AZD_MODE,
|
||||
options_dict=MODE_MAP,
|
||||
options_fn=main_zone_options,
|
||||
translation_key="modes",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
AirzoneSelectDescription(
|
||||
@@ -59,7 +96,19 @@ async def async_setup_entry(
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# Zones
|
||||
async_add_entities(
|
||||
entities: list[AirzoneZoneSelect] = [
|
||||
AirzoneZoneSelect(
|
||||
coordinator,
|
||||
description,
|
||||
zone_id,
|
||||
zone_data,
|
||||
)
|
||||
for description in MAIN_ZONE_SELECT_TYPES
|
||||
for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items()
|
||||
if description.key in zone_data and zone_data.get(AZD_MASTER)
|
||||
]
|
||||
|
||||
entities.extend(
|
||||
AirzoneZoneSelect(
|
||||
coordinator,
|
||||
description,
|
||||
@@ -71,6 +120,8 @@ async def async_setup_entry(
|
||||
if description.key in zone_data
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
|
||||
"""Define an Airzone Cloud select."""
|
||||
@@ -110,6 +161,11 @@ class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
|
||||
|
||||
self._attr_unique_id = f"{zone_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_options = self.entity_description.options_fn(
|
||||
zone_data, description.options_dict
|
||||
)
|
||||
|
||||
self.values_dict = {v: k for k, v in description.options_dict.items()}
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
@@ -36,6 +36,17 @@
|
||||
"on": "On",
|
||||
"auto": "Auto"
|
||||
}
|
||||
},
|
||||
"modes": {
|
||||
"name": "Mode",
|
||||
"state": {
|
||||
"cool": "[%key:component::climate::entity_component::_::state::cool%]",
|
||||
"dry": "[%key:component::climate::entity_component::_::state::dry%]",
|
||||
"fan": "[%key:component::climate::entity_component::_::state::fan_only%]",
|
||||
"heat": "[%key:component::climate::entity_component::_::state::heat%]",
|
||||
"heat_cool": "[%key:component::climate::entity_component::_::state::heat_cool%]",
|
||||
"stop": "Stop"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
115
homeassistant/components/airzone_cloud/switch.py
Normal file
115
homeassistant/components/airzone_cloud/switch.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Support for the Airzone Cloud switch."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Final
|
||||
|
||||
from aioairzone_cloud.const import API_POWER, API_VALUE, AZD_POWER, AZD_ZONES
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AirzoneCloudConfigEntry
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
from .entity import AirzoneEntity, AirzoneZoneEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AirzoneSwitchDescription(SwitchEntityDescription):
|
||||
"""Class to describe an Airzone switch entity."""
|
||||
|
||||
api_param: str
|
||||
|
||||
|
||||
ZONE_SWITCH_TYPES: Final[tuple[AirzoneSwitchDescription, ...]] = (
|
||||
AirzoneSwitchDescription(
|
||||
api_param=API_POWER,
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
key=AZD_POWER,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AirzoneCloudConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Airzone Cloud switch from a config_entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# Zones
|
||||
async_add_entities(
|
||||
AirzoneZoneSwitch(
|
||||
coordinator,
|
||||
description,
|
||||
zone_id,
|
||||
zone_data,
|
||||
)
|
||||
for description in ZONE_SWITCH_TYPES
|
||||
for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items()
|
||||
if description.key in zone_data
|
||||
)
|
||||
|
||||
|
||||
class AirzoneBaseSwitch(AirzoneEntity, SwitchEntity):
|
||||
"""Define an Airzone Cloud switch."""
|
||||
|
||||
entity_description: AirzoneSwitchDescription
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Update attributes when the coordinator updates."""
|
||||
self._async_update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update switch attributes."""
|
||||
self._attr_is_on = self.get_airzone_value(self.entity_description.key)
|
||||
|
||||
|
||||
class AirzoneZoneSwitch(AirzoneZoneEntity, AirzoneBaseSwitch):
|
||||
"""Define an Airzone Cloud Zone switch."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: AirzoneSwitchDescription,
|
||||
zone_id: str,
|
||||
zone_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, zone_id, zone_data)
|
||||
|
||||
self._attr_name = None
|
||||
self._attr_unique_id = f"{zone_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
param = self.entity_description.api_param
|
||||
params: dict[str, Any] = {
|
||||
param: {
|
||||
API_VALUE: True,
|
||||
}
|
||||
}
|
||||
await self._async_update_params(params)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
param = self.entity_description.api_param
|
||||
params: dict[str, Any] = {
|
||||
param: {
|
||||
API_VALUE: False,
|
||||
}
|
||||
}
|
||||
await self._async_update_params(params)
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
@@ -33,6 +34,7 @@ from homeassistant.helpers.deprecation import (
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -49,6 +51,7 @@ from .const import ( # noqa: F401
|
||||
ATTR_CODE_ARM_REQUIRED,
|
||||
DOMAIN,
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
CodeFormat,
|
||||
)
|
||||
|
||||
@@ -142,6 +145,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
||||
"changed_by",
|
||||
"code_arm_required",
|
||||
"supported_features",
|
||||
"alarm_state",
|
||||
}
|
||||
|
||||
|
||||
@@ -149,6 +153,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
||||
"""An abstract class for alarm control entities."""
|
||||
|
||||
entity_description: AlarmControlPanelEntityDescription
|
||||
_attr_alarm_state: AlarmControlPanelState | None = None
|
||||
_attr_changed_by: str | None = None
|
||||
_attr_code_arm_required: bool = True
|
||||
_attr_code_format: CodeFormat | None = None
|
||||
@@ -157,6 +162,78 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
||||
)
|
||||
_alarm_control_panel_option_default_code: str | None = None
|
||||
|
||||
__alarm_legacy_state: bool = False
|
||||
__alarm_legacy_state_reported: bool = False
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
"""Post initialisation processing."""
|
||||
super().__init_subclass__(**kwargs)
|
||||
if any(method in cls.__dict__ for method in ("_attr_state", "state")):
|
||||
# Integrations should use the 'alarm_state' property instead of
|
||||
# setting the state directly.
|
||||
cls.__alarm_legacy_state = True
|
||||
|
||||
def __setattr__(self, __name: str, __value: Any) -> None:
|
||||
"""Set attribute.
|
||||
|
||||
Deprecation warning if setting '_attr_state' directly
|
||||
unless already reported.
|
||||
"""
|
||||
if __name == "_attr_state":
|
||||
if self.__alarm_legacy_state_reported is not True:
|
||||
self._report_deprecated_alarm_state_handling()
|
||||
self.__alarm_legacy_state_reported = True
|
||||
return super().__setattr__(__name, __value)
|
||||
|
||||
@callback
|
||||
def add_to_platform_start(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
platform: EntityPlatform,
|
||||
parallel_updates: asyncio.Semaphore | None,
|
||||
) -> None:
|
||||
"""Start adding an entity to a platform."""
|
||||
super().add_to_platform_start(hass, platform, parallel_updates)
|
||||
if self.__alarm_legacy_state and not self.__alarm_legacy_state_reported:
|
||||
self._report_deprecated_alarm_state_handling()
|
||||
|
||||
@callback
|
||||
def _report_deprecated_alarm_state_handling(self) -> None:
|
||||
"""Report on deprecated handling of alarm state.
|
||||
|
||||
Integrations should implement alarm_state instead of using state directly.
|
||||
"""
|
||||
self.__alarm_legacy_state_reported = True
|
||||
if "custom_components" in type(self).__module__:
|
||||
# Do not report on core integrations as they have been fixed.
|
||||
report_issue = "report it to the custom integration author."
|
||||
_LOGGER.warning(
|
||||
"Entity %s (%s) is setting state directly"
|
||||
" which will stop working in HA Core 2025.11."
|
||||
" Entities should implement the 'alarm_state' property and"
|
||||
" return its state using the AlarmControlPanelState enum, please %s",
|
||||
self.entity_id,
|
||||
type(self),
|
||||
report_issue,
|
||||
)
|
||||
|
||||
@final
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the current state."""
|
||||
if (alarm_state := self.alarm_state) is None:
|
||||
return None
|
||||
return alarm_state
|
||||
|
||||
@cached_property
|
||||
def alarm_state(self) -> AlarmControlPanelState | None:
|
||||
"""Return the current alarm control panel entity state.
|
||||
|
||||
Integrations should overwrite this or use the '_attr_alarm_state'
|
||||
attribute to set the alarm status using the 'AlarmControlPanelState' enum.
|
||||
"""
|
||||
return self._attr_alarm_state
|
||||
|
||||
@final
|
||||
@callback
|
||||
def code_or_default_code(self, code: str | None) -> str | None:
|
||||
|
||||
@@ -17,6 +17,21 @@ ATTR_CHANGED_BY: Final = "changed_by"
|
||||
ATTR_CODE_ARM_REQUIRED: Final = "code_arm_required"
|
||||
|
||||
|
||||
class AlarmControlPanelState(StrEnum):
|
||||
"""Alarm control panel entity states."""
|
||||
|
||||
DISARMED = "disarmed"
|
||||
ARMED_HOME = "armed_home"
|
||||
ARMED_AWAY = "armed_away"
|
||||
ARMED_NIGHT = "armed_night"
|
||||
ARMED_VACATION = "armed_vacation"
|
||||
ARMED_CUSTOM_BYPASS = "armed_custom_bypass"
|
||||
PENDING = "pending"
|
||||
ARMING = "arming"
|
||||
DISARMING = "disarming"
|
||||
TRIGGERED = "triggered"
|
||||
|
||||
|
||||
class CodeFormat(StrEnum):
|
||||
"""Code formats for the Alarm Control Panel."""
|
||||
|
||||
|
||||
@@ -13,13 +13,6 @@ from homeassistant.const import (
|
||||
CONF_DOMAIN,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_TYPE,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_VACATION,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import (
|
||||
@@ -31,7 +24,7 @@ from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
|
||||
from homeassistant.helpers.entity import get_supported_features
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
|
||||
from . import DOMAIN
|
||||
from . import DOMAIN, AlarmControlPanelState
|
||||
from .const import (
|
||||
CONDITION_ARMED_AWAY,
|
||||
CONDITION_ARMED_CUSTOM_BYPASS,
|
||||
@@ -109,19 +102,19 @@ def async_condition_from_config(
|
||||
) -> condition.ConditionCheckerType:
|
||||
"""Create a function to test a device condition."""
|
||||
if config[CONF_TYPE] == CONDITION_TRIGGERED:
|
||||
state = STATE_ALARM_TRIGGERED
|
||||
state = AlarmControlPanelState.TRIGGERED
|
||||
elif config[CONF_TYPE] == CONDITION_DISARMED:
|
||||
state = STATE_ALARM_DISARMED
|
||||
state = AlarmControlPanelState.DISARMED
|
||||
elif config[CONF_TYPE] == CONDITION_ARMED_HOME:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
state = AlarmControlPanelState.ARMED_HOME
|
||||
elif config[CONF_TYPE] == CONDITION_ARMED_AWAY:
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
state = AlarmControlPanelState.ARMED_AWAY
|
||||
elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT:
|
||||
state = STATE_ALARM_ARMED_NIGHT
|
||||
state = AlarmControlPanelState.ARMED_NIGHT
|
||||
elif config[CONF_TYPE] == CONDITION_ARMED_VACATION:
|
||||
state = STATE_ALARM_ARMED_VACATION
|
||||
state = AlarmControlPanelState.ARMED_VACATION
|
||||
elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS:
|
||||
state = STATE_ALARM_ARMED_CUSTOM_BYPASS
|
||||
state = AlarmControlPanelState.ARMED_CUSTOM_BYPASS
|
||||
|
||||
registry = er.async_get(hass)
|
||||
entity_id = er.async_resolve_entity_id(registry, config[ATTR_ENTITY_ID])
|
||||
|
||||
@@ -15,13 +15,6 @@ from homeassistant.const import (
|
||||
CONF_FOR,
|
||||
CONF_PLATFORM,
|
||||
CONF_TYPE,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_VACATION,
|
||||
STATE_ALARM_ARMING,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
@@ -29,7 +22,7 @@ from homeassistant.helpers.entity import get_supported_features
|
||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import DOMAIN
|
||||
from . import DOMAIN, AlarmControlPanelState
|
||||
from .const import AlarmControlPanelEntityFeature
|
||||
|
||||
BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"}
|
||||
@@ -129,19 +122,19 @@ async def async_attach_trigger(
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
if config[CONF_TYPE] == "triggered":
|
||||
to_state = STATE_ALARM_TRIGGERED
|
||||
to_state = AlarmControlPanelState.TRIGGERED
|
||||
elif config[CONF_TYPE] == "disarmed":
|
||||
to_state = STATE_ALARM_DISARMED
|
||||
to_state = AlarmControlPanelState.DISARMED
|
||||
elif config[CONF_TYPE] == "arming":
|
||||
to_state = STATE_ALARM_ARMING
|
||||
to_state = AlarmControlPanelState.ARMING
|
||||
elif config[CONF_TYPE] == "armed_home":
|
||||
to_state = STATE_ALARM_ARMED_HOME
|
||||
to_state = AlarmControlPanelState.ARMED_HOME
|
||||
elif config[CONF_TYPE] == "armed_away":
|
||||
to_state = STATE_ALARM_ARMED_AWAY
|
||||
to_state = AlarmControlPanelState.ARMED_AWAY
|
||||
elif config[CONF_TYPE] == "armed_night":
|
||||
to_state = STATE_ALARM_ARMED_NIGHT
|
||||
to_state = AlarmControlPanelState.ARMED_NIGHT
|
||||
elif config[CONF_TYPE] == "armed_vacation":
|
||||
to_state = STATE_ALARM_ARMED_VACATION
|
||||
to_state = AlarmControlPanelState.ARMED_VACATION
|
||||
|
||||
state_config = {
|
||||
state_trigger.CONF_PLATFORM: "state",
|
||||
|
||||
@@ -16,28 +16,21 @@ from homeassistant.const import (
|
||||
SERVICE_ALARM_ARM_VACATION,
|
||||
SERVICE_ALARM_DISARM,
|
||||
SERVICE_ALARM_TRIGGER,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_VACATION,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.core import Context, HomeAssistant, State
|
||||
|
||||
from . import DOMAIN
|
||||
from . import DOMAIN, AlarmControlPanelState
|
||||
|
||||
_LOGGER: Final = logging.getLogger(__name__)
|
||||
|
||||
VALID_STATES: Final[set[str]] = {
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_VACATION,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
|
||||
AlarmControlPanelState.ARMED_HOME,
|
||||
AlarmControlPanelState.ARMED_NIGHT,
|
||||
AlarmControlPanelState.ARMED_VACATION,
|
||||
AlarmControlPanelState.DISARMED,
|
||||
AlarmControlPanelState.TRIGGERED,
|
||||
}
|
||||
|
||||
|
||||
@@ -65,19 +58,19 @@ async def _async_reproduce_state(
|
||||
|
||||
service_data = {ATTR_ENTITY_ID: state.entity_id}
|
||||
|
||||
if state.state == STATE_ALARM_ARMED_AWAY:
|
||||
if state.state == AlarmControlPanelState.ARMED_AWAY:
|
||||
service = SERVICE_ALARM_ARM_AWAY
|
||||
elif state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
|
||||
elif state.state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS:
|
||||
service = SERVICE_ALARM_ARM_CUSTOM_BYPASS
|
||||
elif state.state == STATE_ALARM_ARMED_HOME:
|
||||
elif state.state == AlarmControlPanelState.ARMED_HOME:
|
||||
service = SERVICE_ALARM_ARM_HOME
|
||||
elif state.state == STATE_ALARM_ARMED_NIGHT:
|
||||
elif state.state == AlarmControlPanelState.ARMED_NIGHT:
|
||||
service = SERVICE_ALARM_ARM_NIGHT
|
||||
elif state.state == STATE_ALARM_ARMED_VACATION:
|
||||
elif state.state == AlarmControlPanelState.ARMED_VACATION:
|
||||
service = SERVICE_ALARM_ARM_VACATION
|
||||
elif state.state == STATE_ALARM_DISARMED:
|
||||
elif state.state == AlarmControlPanelState.DISARMED:
|
||||
service = SERVICE_ALARM_DISARM
|
||||
elif state.state == STATE_ALARM_TRIGGERED:
|
||||
elif state.state == AlarmControlPanelState.TRIGGERED:
|
||||
service = SERVICE_ALARM_TRIGGER
|
||||
|
||||
await hass.services.async_call(
|
||||
|
||||
@@ -7,16 +7,10 @@ import voluptuous as vol
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
CodeFormat,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.const import ATTR_CODE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_platform
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -106,15 +100,15 @@ class AlarmDecoderAlarmPanel(AlarmDecoderEntity, AlarmControlPanelEntity):
|
||||
def _message_callback(self, message):
|
||||
"""Handle received messages."""
|
||||
if message.alarm_sounding or message.fire_alarm:
|
||||
self._attr_state = STATE_ALARM_TRIGGERED
|
||||
self._attr_alarm_state = AlarmControlPanelState.TRIGGERED
|
||||
elif message.armed_away:
|
||||
self._attr_state = STATE_ALARM_ARMED_AWAY
|
||||
self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
|
||||
elif message.armed_home and (message.entry_delay_off or message.perimeter_only):
|
||||
self._attr_state = STATE_ALARM_ARMED_NIGHT
|
||||
self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
|
||||
elif message.armed_home:
|
||||
self._attr_state = STATE_ALARM_ARMED_HOME
|
||||
self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
|
||||
else:
|
||||
self._attr_state = STATE_ALARM_DISARMED
|
||||
self._attr_alarm_state = AlarmControlPanelState.DISARMED
|
||||
|
||||
self._attr_extra_state_attributes = {
|
||||
"ac_power": message.ac_power,
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.components import (
|
||||
)
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
CodeFormat,
|
||||
)
|
||||
from homeassistant.components.climate import HVACMode
|
||||
@@ -36,10 +37,6 @@ from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
PERCENTAGE,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
@@ -1317,13 +1314,13 @@ class AlexaSecurityPanelController(AlexaCapability):
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
arm_state = self.entity.state
|
||||
if arm_state == STATE_ALARM_ARMED_HOME:
|
||||
if arm_state == AlarmControlPanelState.ARMED_HOME:
|
||||
return "ARMED_STAY"
|
||||
if arm_state == STATE_ALARM_ARMED_AWAY:
|
||||
if arm_state == AlarmControlPanelState.ARMED_AWAY:
|
||||
return "ARMED_AWAY"
|
||||
if arm_state == STATE_ALARM_ARMED_NIGHT:
|
||||
if arm_state == AlarmControlPanelState.ARMED_NIGHT:
|
||||
return "ARMED_NIGHT"
|
||||
if arm_state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
|
||||
if arm_state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS:
|
||||
return "ARMED_STAY"
|
||||
return "DISARMED"
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any
|
||||
|
||||
from homeassistant import core as ha
|
||||
from homeassistant.components import (
|
||||
alarm_control_panel,
|
||||
button,
|
||||
camera,
|
||||
climate,
|
||||
@@ -51,7 +52,6 @@ from homeassistant.const import (
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_SET,
|
||||
SERVICE_VOLUME_UP,
|
||||
STATE_ALARM_DISARMED,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.helpers import network
|
||||
@@ -1083,7 +1083,7 @@ async def async_api_arm(
|
||||
arm_state = directive.payload["armState"]
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.state != STATE_ALARM_DISARMED:
|
||||
if entity.state != alarm_control_panel.AlarmControlPanelState.DISARMED:
|
||||
msg = "You must disarm the system before you can set the requested arm state."
|
||||
raise AlexaSecurityPanelAuthorizationRequired(msg)
|
||||
|
||||
@@ -1133,7 +1133,7 @@ async def async_api_disarm(
|
||||
# Per Alexa Documentation: If you receive a Disarm directive, and the
|
||||
# system is already disarmed, respond with a success response,
|
||||
# not an error response.
|
||||
if entity.state == STATE_ALARM_DISARMED:
|
||||
if entity.state == alarm_control_panel.AlarmControlPanelState.DISARMED:
|
||||
return response
|
||||
|
||||
payload = directive.payload
|
||||
|
||||
@@ -27,8 +27,9 @@ from homeassistant.config_entries import SOURCE_IGNORE
|
||||
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.system_info import async_get_system_info
|
||||
from homeassistant.loader import (
|
||||
@@ -136,7 +137,7 @@ class Analytics:
|
||||
@property
|
||||
def supervisor(self) -> bool:
|
||||
"""Return bool if a supervisor is present."""
|
||||
return hassio.is_hassio(self.hass)
|
||||
return is_hassio(self.hass)
|
||||
|
||||
async def load(self) -> None:
|
||||
"""Load preferences."""
|
||||
@@ -369,3 +370,71 @@ class Analytics:
|
||||
for entry in entries
|
||||
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
"""Return the devices payload."""
|
||||
integrations_without_model_id: set[str] = set()
|
||||
devices: list[dict[str, Any]] = []
|
||||
dev_reg = dr.async_get(hass)
|
||||
ignored_integrations = {
|
||||
"bluetooth",
|
||||
"esphome",
|
||||
"hassio",
|
||||
"mqtt",
|
||||
}
|
||||
# Devices that need via device info set
|
||||
new_indexes: dict[str, int] = {}
|
||||
via_devices: dict[str, str] = {}
|
||||
|
||||
for device in dev_reg.devices.values():
|
||||
# Ignore services
|
||||
if device.entry_type:
|
||||
continue
|
||||
|
||||
if not device.primary_config_entry:
|
||||
continue
|
||||
|
||||
config_entry = hass.config_entries.async_get_entry(device.primary_config_entry)
|
||||
|
||||
if config_entry is None:
|
||||
continue
|
||||
|
||||
if config_entry.domain in ignored_integrations:
|
||||
continue
|
||||
|
||||
if not device.model_id:
|
||||
integrations_without_model_id.add(config_entry.domain)
|
||||
continue
|
||||
|
||||
if not device.manufacturer:
|
||||
continue
|
||||
|
||||
new_indexes[device.id] = len(devices)
|
||||
devices.append(
|
||||
{
|
||||
"integration": config_entry.domain,
|
||||
"manufacturer": device.manufacturer,
|
||||
"model_id": device.model_id,
|
||||
"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,
|
||||
}
|
||||
)
|
||||
if device.via_device_id:
|
||||
via_devices[device.id] = device.via_device_id
|
||||
|
||||
for from_device, via_device in via_devices.items():
|
||||
if via_device not in new_indexes:
|
||||
continue
|
||||
devices[new_indexes[from_device]]["via_device"] = new_indexes[via_device]
|
||||
|
||||
return {
|
||||
"version": "home-assistant:1",
|
||||
"no_model_id": sorted(integrations_without_model_id),
|
||||
"devices": devices,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "analytics",
|
||||
"name": "Analytics",
|
||||
"after_dependencies": ["energy", "recorder"],
|
||||
"after_dependencies": ["energy", "hassio", "recorder"],
|
||||
"codeowners": ["@home-assistant/core", "@ludeeus"],
|
||||
"dependencies": ["api", "websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
||||
|
||||
@@ -27,6 +27,7 @@ from homeassistant.helpers.selector import (
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_TRACKED_ADDONS,
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
||||
CONF_TRACKED_INTEGRATIONS,
|
||||
DOMAIN,
|
||||
@@ -55,8 +56,12 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS
|
||||
if all(
|
||||
[
|
||||
not user_input.get(CONF_TRACKED_ADDONS),
|
||||
not user_input.get(CONF_TRACKED_INTEGRATIONS),
|
||||
not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
|
||||
]
|
||||
):
|
||||
errors["base"] = "no_integrations_selected"
|
||||
else:
|
||||
@@ -64,6 +69,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
title="Home Assistant Analytics Insights",
|
||||
data={},
|
||||
options={
|
||||
CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
|
||||
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
||||
CONF_TRACKED_INTEGRATIONS, []
|
||||
),
|
||||
@@ -77,6 +83,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
session=async_get_clientsession(self.hass)
|
||||
)
|
||||
try:
|
||||
addons = await client.get_addons()
|
||||
integrations = await client.get_integrations()
|
||||
custom_integrations = await client.get_custom_integrations()
|
||||
except HomeassistantAnalyticsConnectionError:
|
||||
@@ -99,6 +106,13 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=list(addons),
|
||||
multiple=True,
|
||||
sort=True,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=options,
|
||||
@@ -127,14 +141,19 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Manage the options."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS
|
||||
if all(
|
||||
[
|
||||
not user_input.get(CONF_TRACKED_ADDONS),
|
||||
not user_input.get(CONF_TRACKED_INTEGRATIONS),
|
||||
not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
|
||||
]
|
||||
):
|
||||
errors["base"] = "no_integrations_selected"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="",
|
||||
data={
|
||||
CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
|
||||
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
||||
CONF_TRACKED_INTEGRATIONS, []
|
||||
),
|
||||
@@ -148,6 +167,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
session=async_get_clientsession(self.hass)
|
||||
)
|
||||
try:
|
||||
addons = await client.get_addons()
|
||||
integrations = await client.get_integrations()
|
||||
custom_integrations = await client.get_custom_integrations()
|
||||
except HomeassistantAnalyticsConnectionError:
|
||||
@@ -168,6 +188,13 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=list(addons),
|
||||
multiple=True,
|
||||
sort=True,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=options,
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
|
||||
DOMAIN = "analytics_insights"
|
||||
|
||||
CONF_TRACKED_ADDONS = "tracked_addons"
|
||||
CONF_TRACKED_INTEGRATIONS = "tracked_integrations"
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations"
|
||||
|
||||
|
||||
@@ -12,11 +12,13 @@ from python_homeassistant_analytics import (
|
||||
HomeassistantAnalyticsConnectionError,
|
||||
HomeassistantAnalyticsNotModifiedError,
|
||||
)
|
||||
from python_homeassistant_analytics.models import Addon
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
CONF_TRACKED_ADDONS,
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
||||
CONF_TRACKED_INTEGRATIONS,
|
||||
DOMAIN,
|
||||
@@ -33,6 +35,7 @@ class AnalyticsData:
|
||||
|
||||
active_installations: int
|
||||
reports_integrations: int
|
||||
addons: dict[str, int]
|
||||
core_integrations: dict[str, int]
|
||||
custom_integrations: dict[str, int]
|
||||
|
||||
@@ -53,6 +56,7 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
||||
update_interval=timedelta(hours=12),
|
||||
)
|
||||
self._client = client
|
||||
self._tracked_addons = self.config_entry.options.get(CONF_TRACKED_ADDONS, [])
|
||||
self._tracked_integrations = self.config_entry.options[
|
||||
CONF_TRACKED_INTEGRATIONS
|
||||
]
|
||||
@@ -62,6 +66,7 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
||||
|
||||
async def _async_update_data(self) -> AnalyticsData:
|
||||
try:
|
||||
addons_data = await self._client.get_addons()
|
||||
data = await self._client.get_current_analytics()
|
||||
custom_data = await self._client.get_custom_integrations()
|
||||
except HomeassistantAnalyticsConnectionError as err:
|
||||
@@ -70,6 +75,9 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
||||
) from err
|
||||
except HomeassistantAnalyticsNotModifiedError:
|
||||
return self.data
|
||||
addons = {
|
||||
addon: get_addon_value(addons_data, addon) for addon in self._tracked_addons
|
||||
}
|
||||
core_integrations = {
|
||||
integration: data.integrations.get(integration, 0)
|
||||
for integration in self._tracked_integrations
|
||||
@@ -81,11 +89,19 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
||||
return AnalyticsData(
|
||||
data.active_installations,
|
||||
data.reports_integrations,
|
||||
addons,
|
||||
core_integrations,
|
||||
custom_integrations,
|
||||
)
|
||||
|
||||
|
||||
def get_addon_value(data: dict[str, Addon], name_slug: str) -> int:
|
||||
"""Get addon value."""
|
||||
if name_slug in data:
|
||||
return data[name_slug].total
|
||||
return 0
|
||||
|
||||
|
||||
def get_custom_integration_value(
|
||||
data: dict[str, CustomIntegration], domain: str
|
||||
) -> int:
|
||||
|
||||
@@ -29,6 +29,20 @@ class AnalyticsSensorEntityDescription(SensorEntityDescription):
|
||||
value_fn: Callable[[AnalyticsData], StateType]
|
||||
|
||||
|
||||
def get_addon_entity_description(
|
||||
name_slug: str,
|
||||
) -> AnalyticsSensorEntityDescription:
|
||||
"""Get addon entity description."""
|
||||
return AnalyticsSensorEntityDescription(
|
||||
key=f"addon_{name_slug}_active_installations",
|
||||
translation_key="addons",
|
||||
name=name_slug,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement="active installations",
|
||||
value_fn=lambda data: data.addons.get(name_slug),
|
||||
)
|
||||
|
||||
|
||||
def get_core_integration_entity_description(
|
||||
domain: str, name: str
|
||||
) -> AnalyticsSensorEntityDescription:
|
||||
@@ -89,6 +103,13 @@ async def async_setup_entry(
|
||||
analytics_data.coordinator
|
||||
)
|
||||
entities: list[HomeassistantAnalyticsSensor] = []
|
||||
entities.extend(
|
||||
HomeassistantAnalyticsSensor(
|
||||
coordinator,
|
||||
get_addon_entity_description(addon_name_slug),
|
||||
)
|
||||
for addon_name_slug in coordinator.data.addons
|
||||
)
|
||||
entities.extend(
|
||||
HomeassistantAnalyticsSensor(
|
||||
coordinator,
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"tracked_addons": "Addons",
|
||||
"tracked_integrations": "Integrations",
|
||||
"tracked_custom_integrations": "Custom integrations"
|
||||
},
|
||||
"data_description": {
|
||||
"tracked_addons": "Select the addons you want to track",
|
||||
"tracked_integrations": "Select the integrations you want to track",
|
||||
"tracked_custom_integrations": "Select the custom integrations you want to track"
|
||||
}
|
||||
@@ -24,10 +26,12 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"tracked_addons": "[%key:component::analytics_insights::config::step::user::data::tracked_addons%]",
|
||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]",
|
||||
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]"
|
||||
},
|
||||
"data_description": {
|
||||
"tracked_addons": "[%key:component::analytics_insights::config::step::user::data_description::tracked_addons%]",
|
||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]",
|
||||
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
@@ -40,6 +41,7 @@ from .const import (
|
||||
CONF_ADB_SERVER_IP,
|
||||
CONF_ADB_SERVER_PORT,
|
||||
CONF_ADBKEY,
|
||||
CONF_SCREENCAP_INTERVAL,
|
||||
CONF_STATE_DETECTION_RULES,
|
||||
DEFAULT_ADB_SERVER_PORT,
|
||||
DEVICE_ANDROIDTV,
|
||||
@@ -66,6 +68,8 @@ RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
|
||||
|
||||
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AndroidTVRuntimeData:
|
||||
@@ -157,6 +161,32 @@ async def async_connect_androidtv(
|
||||
return aftv, None
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
_LOGGER.debug(
|
||||
"Migrating configuration from version %s.%s", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
if entry.version == 1:
|
||||
new_options = {**entry.options}
|
||||
|
||||
# Migrate MinorVersion 1 -> MinorVersion 2: New option
|
||||
if entry.minor_version < 2:
|
||||
new_options = {**new_options, CONF_SCREENCAP_INTERVAL: 0}
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, options=new_options, minor_version=2, version=1
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migration to configuration version %s.%s successful",
|
||||
entry.version,
|
||||
entry.minor_version,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool:
|
||||
"""Set up Android Debug Bridge platform."""
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ from .const import (
|
||||
CONF_APPS,
|
||||
CONF_EXCLUDE_UNNAMED_APPS,
|
||||
CONF_GET_SOURCES,
|
||||
CONF_SCREENCAP,
|
||||
CONF_SCREENCAP_INTERVAL,
|
||||
CONF_STATE_DETECTION_RULES,
|
||||
CONF_TURN_OFF_COMMAND,
|
||||
CONF_TURN_ON_COMMAND,
|
||||
@@ -43,7 +43,7 @@ from .const import (
|
||||
DEFAULT_EXCLUDE_UNNAMED_APPS,
|
||||
DEFAULT_GET_SOURCES,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_SCREENCAP,
|
||||
DEFAULT_SCREENCAP_INTERVAL,
|
||||
DEVICE_CLASSES,
|
||||
DOMAIN,
|
||||
PROP_ETHMAC,
|
||||
@@ -76,6 +76,7 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
@callback
|
||||
def _show_setup_form(
|
||||
@@ -253,10 +254,12 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
|
||||
),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_SCREENCAP,
|
||||
default=options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP),
|
||||
): bool,
|
||||
vol.Required(
|
||||
CONF_SCREENCAP_INTERVAL,
|
||||
default=options.get(
|
||||
CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
|
||||
),
|
||||
): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=15)),
|
||||
vol.Optional(
|
||||
CONF_TURN_OFF_COMMAND,
|
||||
description={
|
||||
|
||||
@@ -9,6 +9,7 @@ CONF_APPS = "apps"
|
||||
CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps"
|
||||
CONF_GET_SOURCES = "get_sources"
|
||||
CONF_SCREENCAP = "screencap"
|
||||
CONF_SCREENCAP_INTERVAL = "screencap_interval"
|
||||
CONF_STATE_DETECTION_RULES = "state_detection_rules"
|
||||
CONF_TURN_OFF_COMMAND = "turn_off_command"
|
||||
CONF_TURN_ON_COMMAND = "turn_on_command"
|
||||
@@ -18,7 +19,7 @@ DEFAULT_DEVICE_CLASS = "auto"
|
||||
DEFAULT_EXCLUDE_UNNAMED_APPS = False
|
||||
DEFAULT_GET_SOURCES = True
|
||||
DEFAULT_PORT = 5555
|
||||
DEFAULT_SCREENCAP = True
|
||||
DEFAULT_SCREENCAP_INTERVAL = 5
|
||||
|
||||
DEVICE_ANDROIDTV = "androidtv"
|
||||
DEVICE_FIRETV = "firetv"
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from androidtv.constants import APPS, KEYS
|
||||
from androidtv.setup_async import AndroidTVAsync, FireTVAsync
|
||||
@@ -23,19 +22,19 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import AndroidTVConfigEntry
|
||||
from .const import (
|
||||
CONF_APPS,
|
||||
CONF_EXCLUDE_UNNAMED_APPS,
|
||||
CONF_GET_SOURCES,
|
||||
CONF_SCREENCAP,
|
||||
CONF_SCREENCAP_INTERVAL,
|
||||
CONF_TURN_OFF_COMMAND,
|
||||
CONF_TURN_ON_COMMAND,
|
||||
DEFAULT_EXCLUDE_UNNAMED_APPS,
|
||||
DEFAULT_GET_SOURCES,
|
||||
DEFAULT_SCREENCAP,
|
||||
DEFAULT_SCREENCAP_INTERVAL,
|
||||
DEVICE_ANDROIDTV,
|
||||
SIGNAL_CONFIG_ENTITY,
|
||||
)
|
||||
@@ -48,8 +47,6 @@ ATTR_DEVICE_PATH = "device_path"
|
||||
ATTR_HDMI_INPUT = "hdmi_input"
|
||||
ATTR_LOCAL_PATH = "local_path"
|
||||
|
||||
MIN_TIME_BETWEEN_SCREENCAPS = timedelta(seconds=60)
|
||||
|
||||
SERVICE_ADB_COMMAND = "adb_command"
|
||||
SERVICE_DOWNLOAD = "download"
|
||||
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
|
||||
@@ -125,7 +122,8 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
||||
self._app_name_to_id: dict[str, str] = {}
|
||||
self._get_sources = DEFAULT_GET_SOURCES
|
||||
self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS
|
||||
self._screencap = DEFAULT_SCREENCAP
|
||||
self._screencap_delta: timedelta | None = None
|
||||
self._last_screencap: datetime | None = None
|
||||
self.turn_on_command: str | None = None
|
||||
self.turn_off_command: str | None = None
|
||||
|
||||
@@ -159,7 +157,13 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
||||
self._exclude_unnamed_apps = options.get(
|
||||
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
|
||||
)
|
||||
self._screencap = options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP)
|
||||
screencap_interval: int = options.get(
|
||||
CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
|
||||
)
|
||||
if screencap_interval > 0:
|
||||
self._screencap_delta = timedelta(minutes=screencap_interval)
|
||||
else:
|
||||
self._screencap_delta = None
|
||||
self.turn_off_command = options.get(CONF_TURN_OFF_COMMAND)
|
||||
self.turn_on_command = options.get(CONF_TURN_ON_COMMAND)
|
||||
|
||||
@@ -183,7 +187,7 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
||||
async def _async_get_screencap(self, prev_app_id: str | None = None) -> None:
|
||||
"""Take a screen capture from the device when enabled."""
|
||||
if (
|
||||
not self._screencap
|
||||
not self._screencap_delta
|
||||
or self.state in {MediaPlayerState.OFF, None}
|
||||
or not self.available
|
||||
):
|
||||
@@ -193,11 +197,18 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
||||
force: bool = prev_app_id is not None
|
||||
if force:
|
||||
force = prev_app_id != self._attr_app_id
|
||||
await self._adb_get_screencap(no_throttle=force)
|
||||
await self._adb_get_screencap(force)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCREENCAPS)
|
||||
async def _adb_get_screencap(self, **kwargs: Any) -> None:
|
||||
"""Take a screen capture from the device every 60 seconds."""
|
||||
async def _adb_get_screencap(self, force: bool = False) -> None:
|
||||
"""Take a screen capture from the device every configured minutes."""
|
||||
time_elapsed = self._screencap_delta is not None and (
|
||||
self._last_screencap is None
|
||||
or (utcnow() - self._last_screencap) >= self._screencap_delta
|
||||
)
|
||||
if not (force or time_elapsed):
|
||||
return
|
||||
|
||||
self._last_screencap = utcnow()
|
||||
if media_data := await self._adb_screencap():
|
||||
self._media_image = media_data, "image/png"
|
||||
self._attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16]
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"apps": "Configure applications list",
|
||||
"get_sources": "Retrieve the running apps as the list of sources",
|
||||
"exclude_unnamed_apps": "Exclude apps with unknown name from the sources list",
|
||||
"screencap": "Use screen capture for album art",
|
||||
"screencap_interval": "Interval in minutes between screen capture for album art (set 0 to disable)",
|
||||
"state_detection_rules": "Configure state detection rules",
|
||||
"turn_off_command": "ADB shell turn off command (leave empty for default)",
|
||||
"turn_on_command": "ADB shell turn on command (leave empty for default)"
|
||||
|
||||
@@ -13,7 +13,7 @@ from anova_wifi import (
|
||||
WebsocketFailure,
|
||||
)
|
||||
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
@@ -71,3 +71,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: AnovaConfigEntry) -> bo
|
||||
# Disconnect from WS
|
||||
await entry.runtime_data.api.disconnect_websocket()
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: AnovaConfigEntry) -> bool:
|
||||
"""Migrate entry."""
|
||||
_LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
|
||||
|
||||
if entry.version > 1:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if entry.version == 1 and entry.minor_version == 1:
|
||||
new_data = {**entry.data}
|
||||
if CONF_DEVICES in new_data:
|
||||
new_data.pop(CONF_DEVICES)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, data=new_data, minor_version=2)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -6,7 +6,7 @@ from anova_wifi import AnovaApi, InvalidLogin
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -16,6 +16,7 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Sets up a config flow for Anova."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
@@ -42,8 +43,6 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
|
||||
data={
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
# this can be removed in a migration to 1.2 in 2024.11
|
||||
CONF_DEVICES: [],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -15,12 +15,14 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type AranetConfigEntry = ConfigEntry[
|
||||
PassiveBluetoothProcessorCoordinator[Aranet4Advertisement]
|
||||
]
|
||||
|
||||
|
||||
def _service_info_to_adv(
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
@@ -28,30 +30,25 @@ def _service_info_to_adv(
|
||||
return Aranet4Advertisement(service_info.device, service_info.advertisement)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AranetConfigEntry) -> bool:
|
||||
"""Set up Aranet from a config entry."""
|
||||
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
|
||||
PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=_service_info_to_adv,
|
||||
)
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
update_method=_service_info_to_adv,
|
||||
)
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(
|
||||
coordinator.async_start()
|
||||
) # only start after all platforms have had a chance to subscribe
|
||||
# only start after all platforms have had a chance to subscribe
|
||||
entry.async_on_unload(coordinator.async_start())
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AranetConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -8,12 +8,10 @@ from typing import Any
|
||||
from aranet4.client import Aranet4Advertisement
|
||||
from bleak.backends.device import BLEDevice
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
PassiveBluetoothEntityKey,
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
PassiveBluetoothProcessorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -38,7 +36,8 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import ARANET_MANUFACTURER_NAME, DOMAIN
|
||||
from . import AranetConfigEntry
|
||||
from .const import ARANET_MANUFACTURER_NAME
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -174,20 +173,17 @@ def sensor_update_to_bluetooth_data_update(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
entry: AranetConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Aranet sensors."""
|
||||
coordinator: PassiveBluetoothProcessorCoordinator[Aranet4Advertisement] = hass.data[
|
||||
DOMAIN
|
||||
][entry.entry_id]
|
||||
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
|
||||
entry.async_on_unload(
|
||||
processor.async_add_entities_listener(
|
||||
Aranet4BluetoothSensorEntity, async_add_entities
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_register_processor(processor))
|
||||
entry.async_on_unload(entry.runtime_data.async_register_processor(processor))
|
||||
|
||||
|
||||
class Aranet4BluetoothSensorEntity(
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/autarco",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["autarco==3.0.0"]
|
||||
"requirements": ["autarco==3.1.0"]
|
||||
}
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
"name": "Awair",
|
||||
"codeowners": ["@ahayworth", "@danielsjf"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
"macaddress": "70886B1*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/awair",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["python_awair"],
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["axis==62"],
|
||||
"requirements": ["axis==63"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""The Backup integration."""
|
||||
|
||||
from homeassistant.components.hassio import is_hassio
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DATA_MANAGER, DOMAIN, LOGGER
|
||||
|
||||
@@ -17,6 +17,7 @@ LOGGER = getLogger(__package__)
|
||||
EXCLUDE_FROM_BACKUP = [
|
||||
"__pycache__/*",
|
||||
".DS_Store",
|
||||
".HA_RESTORE",
|
||||
"*.db-shm",
|
||||
"*.log.*",
|
||||
"*.log",
|
||||
|
||||
@@ -16,6 +16,7 @@ from typing import Any, Protocol, cast
|
||||
|
||||
from securetar import SecureTarFile, atomic_contents_add
|
||||
|
||||
from homeassistant.backup_restore import RESTORE_BACKUP_FILE
|
||||
from homeassistant.const import __version__ as HAVERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -123,6 +124,10 @@ class BaseBackupManager(abc.ABC):
|
||||
LOGGER.debug("Loaded %s platforms", len(self.platforms))
|
||||
self.loaded_platforms = True
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_restore_backup(self, slug: str, **kwargs: Any) -> None:
|
||||
"""Restpre a backup."""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_create_backup(self, **kwargs: Any) -> Backup:
|
||||
"""Generate a backup."""
|
||||
@@ -291,6 +296,25 @@ class BackupManager(BaseBackupManager):
|
||||
|
||||
return tar_file_path.stat().st_size
|
||||
|
||||
async def async_restore_backup(self, slug: str, **kwargs: Any) -> None:
|
||||
"""Restore a backup.
|
||||
|
||||
This will write the restore information to .HA_RESTORE which
|
||||
will be handled during startup by the restore_backup module.
|
||||
"""
|
||||
if (backup := await self.async_get_backup(slug=slug)) is None:
|
||||
raise HomeAssistantError(f"Backup {slug} not found")
|
||||
|
||||
def _write_restore_file() -> None:
|
||||
"""Write the restore file."""
|
||||
Path(self.hass.config.path(RESTORE_BACKUP_FILE)).write_text(
|
||||
f"{backup.path.as_posix()};",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
await self.hass.async_add_executor_job(_write_restore_file)
|
||||
await self.hass.services.async_call("homeassistant", "restart", {})
|
||||
|
||||
|
||||
def _generate_slug(date: str, name: str) -> str:
|
||||
"""Generate a backup slug."""
|
||||
|
||||
@@ -22,6 +22,7 @@ def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) ->
|
||||
websocket_api.async_register_command(hass, handle_info)
|
||||
websocket_api.async_register_command(hass, handle_create)
|
||||
websocket_api.async_register_command(hass, handle_remove)
|
||||
websocket_api.async_register_command(hass, handle_restore)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@@ -85,6 +86,24 @@ async def handle_remove(
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "backup/restore",
|
||||
vol.Required("slug"): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def handle_restore(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Restore a backup."""
|
||||
await hass.data[DATA_MANAGER].async_restore_backup(msg["slug"])
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({vol.Required("type"): "backup/generate"})
|
||||
@websocket_api.async_response
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import CONF_SYNC_TIME, DEFAULT_SYNC_TIME, DOMAIN
|
||||
from .const import CONF_SYNC_TIME, DEFAULT_SYNC_TIME
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,8 +30,10 @@ PLATFORMS = [
|
||||
KEEP_ALIVE_INTERVAL = timedelta(minutes=1)
|
||||
SYNC_TIME_INTERVAL = timedelta(hours=1)
|
||||
|
||||
type BalboaConfigEntry = ConfigEntry[SpaClient]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BalboaConfigEntry) -> bool:
|
||||
"""Set up Balboa Spa from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
|
||||
@@ -44,41 +46,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.error("Failed to get spa info at %s", host)
|
||||
raise ConfigEntryNotReady("Unable to configure")
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = spa
|
||||
entry.runtime_data = spa
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
await async_setup_time_sync(hass, entry)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
entry.async_on_unload(spa.disconnect)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: BalboaConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
_LOGGER.debug("Disconnecting from spa")
|
||||
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
await spa.disconnect()
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
async def update_listener(hass: HomeAssistant, entry: BalboaConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_setup_time_sync(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
async def async_setup_time_sync(hass: HomeAssistant, entry: BalboaConfigEntry) -> None:
|
||||
"""Set up the time sync."""
|
||||
if not entry.options.get(CONF_SYNC_TIME, DEFAULT_SYNC_TIME):
|
||||
return
|
||||
|
||||
_LOGGER.debug("Setting up daily time sync")
|
||||
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
|
||||
spa = entry.runtime_data
|
||||
|
||||
async def sync_time(now: datetime) -> None:
|
||||
now = dt_util.as_local(now)
|
||||
|
||||
@@ -12,19 +12,20 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import BalboaConfigEntry
|
||||
from .entity import BalboaEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: BalboaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the spa's binary sensors."""
|
||||
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
|
||||
spa = entry.runtime_data
|
||||
entities = [
|
||||
BalboaBinarySensorEntity(spa, description)
|
||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
PRECISION_HALVES,
|
||||
@@ -24,6 +23,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import BalboaConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .entity import BalboaEntity
|
||||
|
||||
@@ -45,10 +45,12 @@ TEMPERATURE_UNIT_MAP = {
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: BalboaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the spa climate entity."""
|
||||
async_add_entities([BalboaClimateEntity(hass.data[DOMAIN][entry.entry_id])])
|
||||
async_add_entities([BalboaClimateEntity(entry.runtime_data)])
|
||||
|
||||
|
||||
class BalboaClimateEntity(BalboaEntity, ClimateEntity):
|
||||
|
||||
@@ -5,11 +5,10 @@ from __future__ import annotations
|
||||
import math
|
||||
from typing import Any, cast
|
||||
|
||||
from pybalboa import SpaClient, SpaControl
|
||||
from pybalboa import SpaControl
|
||||
from pybalboa.enums import OffOnState, UnknownState
|
||||
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.percentage import (
|
||||
@@ -17,15 +16,17 @@ from homeassistant.util.percentage import (
|
||||
ranged_value_to_percentage,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import BalboaConfigEntry
|
||||
from .entity import BalboaEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: BalboaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the spa's pumps."""
|
||||
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
|
||||
spa = entry.runtime_data
|
||||
async_add_entities(BalboaPumpFanEntity(control) for control in spa.pumps)
|
||||
|
||||
|
||||
|
||||
@@ -4,23 +4,24 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from pybalboa import SpaClient, SpaControl
|
||||
from pybalboa import SpaControl
|
||||
from pybalboa.enums import OffOnState, UnknownState
|
||||
|
||||
from homeassistant.components.light import ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import BalboaConfigEntry
|
||||
from .entity import BalboaEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: BalboaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the spa's lights."""
|
||||
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
|
||||
spa = entry.runtime_data
|
||||
async_add_entities(BalboaLightEntity(control) for control in spa.lights)
|
||||
|
||||
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
"""Support for Spa Client selects."""
|
||||
|
||||
from pybalboa import SpaClient, SpaControl
|
||||
from pybalboa import SpaControl
|
||||
from pybalboa.enums import LowHighRange
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import BalboaConfigEntry
|
||||
from .entity import BalboaEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: BalboaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the spa select entity."""
|
||||
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
|
||||
spa = entry.runtime_data
|
||||
async_add_entities([BalboaTempRangeSelectEntity(spa.temperature_range)])
|
||||
|
||||
|
||||
|
||||
@@ -31,10 +31,12 @@ class BangOlufsenData:
|
||||
client: MozartClient
|
||||
|
||||
|
||||
type BangOlufsenConfigEntry = ConfigEntry[BangOlufsenData]
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry) -> bool:
|
||||
"""Set up from a config entry."""
|
||||
|
||||
# Remove casts to str
|
||||
@@ -67,10 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
websocket = BangOlufsenWebsocket(hass, entry, client)
|
||||
|
||||
# Add the websocket and API client
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BangOlufsenData(
|
||||
websocket,
|
||||
client,
|
||||
)
|
||||
entry.runtime_data = BangOlufsenData(websocket, client)
|
||||
|
||||
# Start WebSocket connection
|
||||
await client.connect_notifications(remote_control=True, reconnect=True)
|
||||
@@ -80,15 +79,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: BangOlufsenConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
# Close the API client and WebSocket notification listener
|
||||
hass.data[DOMAIN][entry.entry_id].client.disconnect_notifications()
|
||||
await hass.data[DOMAIN][entry.entry_id].client.close_api_client()
|
||||
entry.runtime_data.client.disconnect_notifications()
|
||||
await entry.runtime_data.client.close_api_client()
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -7,20 +7,56 @@ from typing import Final
|
||||
|
||||
from mozart_api.models import Source, SourceArray, SourceTypeEnum
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerState, MediaType
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerState,
|
||||
MediaType,
|
||||
RepeatMode,
|
||||
)
|
||||
|
||||
|
||||
class BangOlufsenSource:
|
||||
"""Class used for associating device source ids with friendly names. May not include all sources."""
|
||||
|
||||
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
|
||||
BLUETOOTH: Final[Source] = Source(name="Bluetooth", id="bluetooth")
|
||||
CHROMECAST: Final[Source] = Source(name="Chromecast built-in", id="chromeCast")
|
||||
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
|
||||
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
|
||||
NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
|
||||
DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
|
||||
TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
|
||||
URI_STREAMER: Final[Source] = Source(
|
||||
name="Audio Streamer",
|
||||
id="uriStreamer",
|
||||
is_seekable=False,
|
||||
)
|
||||
BLUETOOTH: Final[Source] = Source(
|
||||
name="Bluetooth",
|
||||
id="bluetooth",
|
||||
is_seekable=False,
|
||||
)
|
||||
CHROMECAST: Final[Source] = Source(
|
||||
name="Chromecast built-in",
|
||||
id="chromeCast",
|
||||
is_seekable=False,
|
||||
)
|
||||
LINE_IN: Final[Source] = Source(
|
||||
name="Line-In",
|
||||
id="lineIn",
|
||||
is_seekable=False,
|
||||
)
|
||||
SPDIF: Final[Source] = Source(
|
||||
name="Optical",
|
||||
id="spdif",
|
||||
is_seekable=False,
|
||||
)
|
||||
NET_RADIO: Final[Source] = Source(
|
||||
name="B&O Radio",
|
||||
id="netRadio",
|
||||
is_seekable=False,
|
||||
)
|
||||
DEEZER: Final[Source] = Source(
|
||||
name="Deezer",
|
||||
id="deezer",
|
||||
is_seekable=True,
|
||||
)
|
||||
TIDAL: Final[Source] = Source(
|
||||
name="Tidal",
|
||||
id="tidal",
|
||||
is_seekable=True,
|
||||
)
|
||||
|
||||
|
||||
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
||||
@@ -36,6 +72,17 @@ BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
||||
"unknown": MediaPlayerState.IDLE,
|
||||
}
|
||||
|
||||
# Dict used for translating Home Assistant settings to device repeat settings.
|
||||
BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
|
||||
RepeatMode.ALL: "all",
|
||||
RepeatMode.ONE: "track",
|
||||
RepeatMode.OFF: "none",
|
||||
}
|
||||
# Dict used for translating device repeat settings to Home Assistant settings.
|
||||
BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
|
||||
value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
|
||||
}
|
||||
|
||||
|
||||
# Media types for play_media
|
||||
class BangOlufsenMediaType(StrEnum):
|
||||
@@ -147,6 +194,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
is_playable=False,
|
||||
name="Audio Streamer",
|
||||
type=SourceTypeEnum(value="uriStreamer"),
|
||||
is_seekable=False,
|
||||
),
|
||||
Source(
|
||||
id="bluetooth",
|
||||
@@ -154,6 +202,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
is_playable=False,
|
||||
name="Bluetooth",
|
||||
type=SourceTypeEnum(value="bluetooth"),
|
||||
is_seekable=False,
|
||||
),
|
||||
Source(
|
||||
id="spotify",
|
||||
@@ -161,6 +210,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
is_playable=False,
|
||||
name="Spotify Connect",
|
||||
type=SourceTypeEnum(value="spotify"),
|
||||
is_seekable=True,
|
||||
),
|
||||
Source(
|
||||
id="lineIn",
|
||||
@@ -168,6 +218,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
is_playable=True,
|
||||
name="Line-In",
|
||||
type=SourceTypeEnum(value="lineIn"),
|
||||
is_seekable=False,
|
||||
),
|
||||
Source(
|
||||
id="spdif",
|
||||
@@ -175,6 +226,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
is_playable=True,
|
||||
name="Optical",
|
||||
type=SourceTypeEnum(value="spdif"),
|
||||
is_seekable=False,
|
||||
),
|
||||
Source(
|
||||
id="netRadio",
|
||||
@@ -182,6 +234,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
is_playable=True,
|
||||
name="B&O Radio",
|
||||
type=SourceTypeEnum(value="netRadio"),
|
||||
is_seekable=False,
|
||||
),
|
||||
Source(
|
||||
id="deezer",
|
||||
@@ -189,6 +242,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
is_playable=True,
|
||||
name="Deezer",
|
||||
type=SourceTypeEnum(value="deezer"),
|
||||
is_seekable=True,
|
||||
),
|
||||
Source(
|
||||
id="tidalConnect",
|
||||
@@ -196,6 +250,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
||||
is_playable=True,
|
||||
name="Tidal Connect",
|
||||
type=SourceTypeEnum(value="tidalConnect"),
|
||||
is_seekable=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import contextlib
|
||||
from datetime import timedelta
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from aiohttp import ClientConnectorError
|
||||
from mozart_api import __version__ as MOZART_API_VERSION
|
||||
from mozart_api.exceptions import ApiException
|
||||
from mozart_api.models import (
|
||||
@@ -22,6 +25,7 @@ from mozart_api.models import (
|
||||
PlaybackProgress,
|
||||
PlayQueueItem,
|
||||
PlayQueueItemType,
|
||||
PlayQueueSettings,
|
||||
RenderingState,
|
||||
SceneProperties,
|
||||
SoftwareUpdateState,
|
||||
@@ -44,6 +48,7 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
MediaType,
|
||||
RepeatMode,
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -56,8 +61,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import BangOlufsenData
|
||||
from . import BangOlufsenConfigEntry
|
||||
from .const import (
|
||||
BANG_OLUFSEN_REPEAT_FROM_HA,
|
||||
BANG_OLUFSEN_REPEAT_TO_HA,
|
||||
BANG_OLUFSEN_STATES,
|
||||
CONF_BEOLINK_JID,
|
||||
CONNECTION_STATUS,
|
||||
@@ -72,6 +79,8 @@ from .const import (
|
||||
from .entity import BangOlufsenEntity
|
||||
from .util import get_serial_number_from_jid
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BANG_OLUFSEN_FEATURES = (
|
||||
@@ -84,8 +93,9 @@ BANG_OLUFSEN_FEATURES = (
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
| MediaPlayerEntityFeature.SEEK
|
||||
| MediaPlayerEntityFeature.REPEAT_SET
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.SHUFFLE_SET
|
||||
| MediaPlayerEntityFeature.STOP
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
@@ -96,14 +106,16 @@ BANG_OLUFSEN_FEATURES = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BangOlufsenConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a Media Player entity from config entry."""
|
||||
data: BangOlufsenData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
# Add MediaPlayer entity
|
||||
async_add_entities(new_entities=[BangOlufsenMediaPlayer(config_entry, data.client)])
|
||||
async_add_entities(
|
||||
new_entities=[
|
||||
BangOlufsenMediaPlayer(config_entry, config_entry.runtime_data.client)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
@@ -112,7 +124,6 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
_attr_icon = "mdi:speaker-wireless"
|
||||
_attr_name = None
|
||||
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||
_attr_supported_features = BANG_OLUFSEN_FEATURES
|
||||
|
||||
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
|
||||
"""Initialize the media player."""
|
||||
@@ -129,6 +140,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
serial_number=self._unique_id,
|
||||
)
|
||||
self._attr_unique_id = self._unique_id
|
||||
self._attr_should_poll = True
|
||||
|
||||
# Misc. variables.
|
||||
self._audio_sources: dict[str, str] = {}
|
||||
@@ -218,6 +230,19 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
|
||||
await self._async_update_sound_modes()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update queue settings."""
|
||||
# The WebSocket event listener is the main handler for connection state.
|
||||
# The polling updates do therefore not set the device as available or unavailable
|
||||
with contextlib.suppress(ApiException, ClientConnectorError, TimeoutError):
|
||||
queue_settings = await self._client.get_settings_queue(_request_timeout=5)
|
||||
|
||||
if queue_settings.repeat is not None:
|
||||
self._attr_repeat = BANG_OLUFSEN_REPEAT_TO_HA[queue_settings.repeat]
|
||||
|
||||
if queue_settings.shuffle is not None:
|
||||
self._attr_shuffle = queue_settings.shuffle
|
||||
|
||||
async def _async_update_sources(self) -> None:
|
||||
"""Get sources for the specific product."""
|
||||
|
||||
@@ -462,6 +487,17 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
||||
"""Flag media player features that are supported."""
|
||||
features = BANG_OLUFSEN_FEATURES
|
||||
|
||||
# Add seeking if supported by the current source
|
||||
if self._source_change.is_seekable is True:
|
||||
features |= MediaPlayerEntityFeature.SEEK
|
||||
|
||||
return features
|
||||
|
||||
@property
|
||||
def state(self) -> MediaPlayerState:
|
||||
"""Return the current state of the media player."""
|
||||
@@ -608,17 +644,12 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
|
||||
async def async_media_seek(self, position: float) -> None:
|
||||
"""Seek to position in ms."""
|
||||
if self._source_change.id == BangOlufsenSource.DEEZER.id:
|
||||
await self._client.seek_to_position(position_ms=int(position * 1000))
|
||||
# Try to prevent the playback progress from bouncing in the UI.
|
||||
self._attr_media_position_updated_at = utcnow()
|
||||
self._playback_progress = PlaybackProgress(progress=int(position))
|
||||
await self._client.seek_to_position(position_ms=int(position * 1000))
|
||||
# Try to prevent the playback progress from bouncing in the UI.
|
||||
self._attr_media_position_updated_at = utcnow()
|
||||
self._playback_progress = PlaybackProgress(progress=int(position))
|
||||
|
||||
self.async_write_ha_state()
|
||||
else:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="non_deezer_seeking"
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send the previous track command."""
|
||||
@@ -628,6 +659,20 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
"""Clear the current playback queue."""
|
||||
await self._client.post_clear_queue()
|
||||
|
||||
async def async_set_repeat(self, repeat: RepeatMode) -> None:
|
||||
"""Set playback queues to repeat."""
|
||||
await self._client.set_settings_queue(
|
||||
play_queue_settings=PlayQueueSettings(
|
||||
repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
|
||||
)
|
||||
)
|
||||
|
||||
async def async_set_shuffle(self, shuffle: bool) -> None:
|
||||
"""Set playback queues to shuffle."""
|
||||
await self._client.set_settings_queue(
|
||||
play_queue_settings=PlayQueueSettings(shuffle=shuffle),
|
||||
)
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select an input source."""
|
||||
if source not in self._sources.values():
|
||||
|
||||
@@ -29,9 +29,6 @@
|
||||
"m3u_invalid_format": {
|
||||
"message": "Media sources with the .m3u extension are not supported."
|
||||
},
|
||||
"non_deezer_seeking": {
|
||||
"message": "Seeking is currently only supported when using Deezer"
|
||||
},
|
||||
"invalid_source": {
|
||||
"message": "Invalid source: {invalid_source}. Valid sources are: {valid_sources}"
|
||||
},
|
||||
|
||||
@@ -17,9 +17,11 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT
|
||||
from .const import DEFAULT_SETUP_TIMEOUT
|
||||
from .helpers import get_maybe_authenticated_session
|
||||
|
||||
type BleBoxConfigEntry = ConfigEntry[Box]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [
|
||||
@@ -35,7 +37,7 @@ PLATFORMS = [
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BleBoxConfigEntry) -> bool:
|
||||
"""Set up BleBox devices from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
port = entry.data[CONF_PORT]
|
||||
@@ -55,20 +57,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.error("Identify failed at %s:%d (%s)", api_host.host, api_host.port, ex)
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
domain = hass.data.setdefault(DOMAIN, {})
|
||||
domain_entry = domain.setdefault(entry.entry_id, {})
|
||||
product = domain_entry.setdefault(PRODUCT, product)
|
||||
entry.runtime_data = product
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: BleBoxConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
"""BleBox binary sensor entities."""
|
||||
|
||||
from blebox_uniapi.binary_sensor import BinarySensor as BinarySensorFeature
|
||||
from blebox_uniapi.box import Box
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, PRODUCT
|
||||
from . import BleBoxConfigEntry
|
||||
from .entity import BleBoxEntity
|
||||
|
||||
BINARY_SENSOR_TYPES = (
|
||||
@@ -25,15 +23,13 @@ BINARY_SENSOR_TYPES = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BleBoxConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a BleBox entry."""
|
||||
|
||||
product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
|
||||
entities = [
|
||||
BleBoxBinarySensorEntity(feature, description)
|
||||
for feature in product.features.get("binary_sensors", [])
|
||||
for feature in config_entry.runtime_data.features.get("binary_sensors", [])
|
||||
for description in BINARY_SENSOR_TYPES
|
||||
if description.key == feature.device_class
|
||||
]
|
||||
|
||||
@@ -2,28 +2,25 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from blebox_uniapi.box import Box
|
||||
import blebox_uniapi.button
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, PRODUCT
|
||||
from . import BleBoxConfigEntry
|
||||
from .entity import BleBoxEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BleBoxConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a BleBox button entry."""
|
||||
product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
|
||||
|
||||
entities = [
|
||||
BleBoxButtonEntity(feature) for feature in product.features.get("buttons", [])
|
||||
BleBoxButtonEntity(feature)
|
||||
for feature in config_entry.runtime_data.features.get("buttons", [])
|
||||
]
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from blebox_uniapi.box import Box
|
||||
import blebox_uniapi.climate
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
@@ -12,12 +11,11 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, PRODUCT
|
||||
from . import BleBoxConfigEntry
|
||||
from .entity import BleBoxEntity
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
@@ -39,14 +37,13 @@ BLEBOX_TO_HVACACTION = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BleBoxConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a BleBox climate entity."""
|
||||
product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
|
||||
|
||||
entities = [
|
||||
BleBoxClimateEntity(feature) for feature in product.features.get("climates", [])
|
||||
BleBoxClimateEntity(feature)
|
||||
for feature in config_entry.runtime_data.features.get("climates", [])
|
||||
]
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Constants for the BleBox devices integration."""
|
||||
|
||||
DOMAIN = "blebox"
|
||||
PRODUCT = "product"
|
||||
|
||||
DEFAULT_SETUP_TIMEOUT = 10
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from blebox_uniapi.box import Box
|
||||
import blebox_uniapi.cover
|
||||
from blebox_uniapi.cover import BleboxCoverState
|
||||
|
||||
@@ -16,11 +15,10 @@ from homeassistant.components.cover import (
|
||||
CoverEntityFeature,
|
||||
CoverState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, PRODUCT
|
||||
from . import BleBoxConfigEntry
|
||||
from .entity import BleBoxEntity
|
||||
|
||||
BLEBOX_TO_COVER_DEVICE_CLASSES = {
|
||||
@@ -46,13 +44,13 @@ BLEBOX_TO_HASS_COVER_STATES = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BleBoxConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a BleBox entry."""
|
||||
product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
|
||||
entities = [
|
||||
BleBoxCoverEntity(feature) for feature in product.features.get("covers", [])
|
||||
BleBoxCoverEntity(feature)
|
||||
for feature in config_entry.runtime_data.features.get("covers", [])
|
||||
]
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from blebox_uniapi.box import Box
|
||||
import blebox_uniapi.light
|
||||
from blebox_uniapi.light import BleboxColorMode
|
||||
|
||||
@@ -21,11 +20,10 @@ from homeassistant.components.light import (
|
||||
LightEntity,
|
||||
LightEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, PRODUCT
|
||||
from . import BleBoxConfigEntry
|
||||
from .entity import BleBoxEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,13 +33,13 @@ SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BleBoxConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a BleBox entry."""
|
||||
product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
|
||||
entities = [
|
||||
BleBoxLightEntity(feature) for feature in product.features.get("lights", [])
|
||||
BleBoxLightEntity(feature)
|
||||
for feature in config_entry.runtime_data.features.get("lights", [])
|
||||
]
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""BleBox sensor entities."""
|
||||
|
||||
from blebox_uniapi.box import Box
|
||||
import blebox_uniapi.sensor
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -9,7 +8,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
LIGHT_LUX,
|
||||
@@ -27,7 +25,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, PRODUCT
|
||||
from . import BleBoxConfigEntry
|
||||
from .entity import BleBoxEntity
|
||||
|
||||
SENSOR_TYPES = (
|
||||
@@ -117,14 +115,13 @@ SENSOR_TYPES = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BleBoxConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a BleBox entry."""
|
||||
product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
|
||||
entities = [
|
||||
BleBoxSensorEntity(feature, description)
|
||||
for feature in product.features.get("sensors", [])
|
||||
for feature in config_entry.runtime_data.features.get("sensors", [])
|
||||
for description in SENSOR_TYPES
|
||||
if description.key == feature.device_class
|
||||
]
|
||||
|
||||
@@ -3,15 +3,13 @@
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from blebox_uniapi.box import Box
|
||||
import blebox_uniapi.switch
|
||||
|
||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, PRODUCT
|
||||
from . import BleBoxConfigEntry
|
||||
from .entity import BleBoxEntity
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
@@ -19,13 +17,13 @@ SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BleBoxConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a BleBox switch entity."""
|
||||
product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
|
||||
entities = [
|
||||
BleBoxSwitchEntity(feature) for feature in product.features.get("switches", [])
|
||||
BleBoxSwitchEntity(feature)
|
||||
for feature in config_entry.runtime_data.features.get("switches", [])
|
||||
]
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from blinkpy.auth import Auth
|
||||
@@ -9,7 +10,6 @@ from blinkpy.blinkpy import Blink
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_FILE_PATH,
|
||||
CONF_FILENAME,
|
||||
@@ -24,7 +24,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
|
||||
from .services import setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -40,13 +40,11 @@ SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def _reauth_flow_wrapper(hass, data):
|
||||
async def _reauth_flow_wrapper(
|
||||
hass: HomeAssistant, entry: BlinkConfigEntry, data: dict[str, Any]
|
||||
) -> None:
|
||||
"""Reauth flow wrapper."""
|
||||
hass.add_job(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_REAUTH}, data=data
|
||||
)
|
||||
)
|
||||
entry.async_start_reauth(hass, data=data)
|
||||
persistent_notification.async_create(
|
||||
hass,
|
||||
(
|
||||
@@ -57,16 +55,16 @@ async def _reauth_flow_wrapper(hass, data):
|
||||
)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> bool:
|
||||
"""Handle migration of a previous version config entry."""
|
||||
_LOGGER.debug("Migrating from version %s", entry.version)
|
||||
data = {**entry.data}
|
||||
if entry.version == 1:
|
||||
data.pop("login_response", None)
|
||||
await _reauth_flow_wrapper(hass, data)
|
||||
await _reauth_flow_wrapper(hass, entry, data)
|
||||
return False
|
||||
if entry.version == 2:
|
||||
await _reauth_flow_wrapper(hass, data)
|
||||
await _reauth_flow_wrapper(hass, entry, data)
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -79,10 +77,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> bool:
|
||||
"""Set up Blink via config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
_async_import_options_from_data_if_missing(hass, entry)
|
||||
session = async_get_clientsession(hass)
|
||||
blink = Blink(session=session)
|
||||
@@ -104,7 +100,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -113,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@callback
|
||||
def _async_import_options_from_data_if_missing(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
hass: HomeAssistant, entry: BlinkConfigEntry
|
||||
) -> None:
|
||||
options = dict(entry.options)
|
||||
if CONF_SCAN_INTERVAL not in entry.options:
|
||||
@@ -123,8 +120,6 @@ def _async_import_options_from_data_if_missing(
|
||||
hass.config_entries.async_update_entry(entry, options=options)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> bool:
|
||||
"""Unload Blink entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -9,13 +9,9 @@ from blinkpy.blinkpy import Blink, BlinkSyncModule
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_DISARMED,
|
||||
)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -23,16 +19,18 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
config_entry: BlinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Blink Alarm Control Panels."""
|
||||
coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
sync_modules = []
|
||||
for sync_name, sync_module in coordinator.api.sync.items():
|
||||
@@ -80,8 +78,10 @@ class BlinkSyncModuleHA(
|
||||
self.sync.attributes["associated_cameras"] = list(self.sync.cameras)
|
||||
self.sync.attributes[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION
|
||||
self._attr_extra_state_attributes = self.sync.attributes
|
||||
self._attr_state = (
|
||||
STATE_ALARM_ARMED_AWAY if self.sync.arm else STATE_ALARM_DISARMED
|
||||
self._attr_alarm_state = (
|
||||
AlarmControlPanelState.ARMED_AWAY
|
||||
if self.sync.arm
|
||||
else AlarmControlPanelState.DISARMED
|
||||
)
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
|
||||
@@ -9,7 +9,6 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -23,7 +22,7 @@ from .const import (
|
||||
TYPE_CAMERA_ARMED,
|
||||
TYPE_MOTION_DETECTED,
|
||||
)
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -47,11 +46,13 @@ BINARY_SENSORS_TYPES: tuple[BinarySensorEntityDescription, ...] = (
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
config_entry: BlinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the blink binary sensors."""
|
||||
|
||||
coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
entities = [
|
||||
BlinkBinarySensor(coordinator, camera, description)
|
||||
|
||||
@@ -10,7 +10,6 @@ from requests.exceptions import ChunkedEncodingError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
@@ -28,7 +27,7 @@ from .const import (
|
||||
SERVICE_SAVE_VIDEO,
|
||||
SERVICE_TRIGGER,
|
||||
)
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,11 +37,13 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
config_entry: BlinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a Blink Camera."""
|
||||
|
||||
coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
entities = [
|
||||
BlinkCamera(coordinator, name, camera)
|
||||
for name, camera in coordinator.api.cameras.items()
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any
|
||||
|
||||
from blinkpy.blinkpy import Blink
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
@@ -16,6 +17,8 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = 300
|
||||
|
||||
type BlinkConfigEntry = ConfigEntry[BlinkUpdateCoordinator]
|
||||
|
||||
|
||||
class BlinkUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""BlinkUpdateCoordinator - In charge of downloading the data for a site."""
|
||||
|
||||
@@ -4,24 +4,21 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from blinkpy.blinkpy import Blink
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import BlinkConfigEntry
|
||||
|
||||
TO_REDACT = {"serial", "macaddress", "username", "password", "token", "unique_id"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: BlinkConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
api: Blink = hass.data[DOMAIN][config_entry.entry_id].api
|
||||
api = config_entry.runtime_data.api
|
||||
|
||||
data = {
|
||||
camera.name: dict(camera.attributes.items())
|
||||
|
||||
@@ -10,7 +10,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -18,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEFAULT_BRAND, DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,11 +39,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
config_entry: BlinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize a Blink sensor."""
|
||||
|
||||
coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
entities = [
|
||||
BlinkSensor(coordinator, camera, description)
|
||||
for camera in coordinator.api.cameras
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_SEND_PIN
|
||||
from .coordinator import BlinkConfigEntry
|
||||
|
||||
SERVICE_UPDATE_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -30,6 +31,7 @@ def setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
async def send_pin(call: ServiceCall):
|
||||
"""Call blink to send new pin."""
|
||||
config_entry: BlinkConfigEntry | None
|
||||
for entry_id in call.data[ATTR_CONFIG_ENTRY_ID]:
|
||||
if not (config_entry := hass.config_entries.async_get_entry(entry_id)):
|
||||
raise ServiceValidationError(
|
||||
@@ -43,7 +45,7 @@ def setup_services(hass: HomeAssistant) -> None:
|
||||
translation_key="not_loaded",
|
||||
translation_placeholders={"target": config_entry.title},
|
||||
)
|
||||
coordinator = hass.data[DOMAIN][entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
await coordinator.api.auth.send_auth_key(
|
||||
coordinator.api,
|
||||
call.data[CONF_PIN],
|
||||
|
||||
@@ -9,7 +9,6 @@ from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -17,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEFAULT_BRAND, DOMAIN, TYPE_CAMERA_ARMED
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
|
||||
|
||||
SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
|
||||
SwitchEntityDescription(
|
||||
@@ -30,11 +29,11 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
config_entry: BlinkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Blink switches."""
|
||||
coordinator: BlinkUpdateCoordinator = hass.data[DOMAIN][config.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
BlinkSwitch(coordinator, camera, description)
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
"""Support for BloomSky weather station."""
|
||||
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.SENSOR]
|
||||
|
||||
DOMAIN = "bloomsky"
|
||||
|
||||
# The BloomSky only updates every 5-8 minutes as per the API spec so there's
|
||||
# no point in polling the API more frequently
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema({vol.Required(CONF_API_KEY): cv.string})}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the BloomSky integration."""
|
||||
api_key = config[DOMAIN][CONF_API_KEY]
|
||||
|
||||
try:
|
||||
bloomsky = BloomSky(api_key, hass.config.units is METRIC_SYSTEM)
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN] = bloomsky
|
||||
|
||||
for platform in PLATFORMS:
|
||||
discovery.load_platform(hass, platform, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class BloomSky:
|
||||
"""Handle all communication with the BloomSky API."""
|
||||
|
||||
# API documentation at http://weatherlution.com/bloomsky-api/
|
||||
API_URL = "http://api.bloomsky.com/api/skydata"
|
||||
|
||||
def __init__(self, api_key, is_metric):
|
||||
"""Initialize the BookSky."""
|
||||
self._api_key = api_key
|
||||
self._endpoint_argument = "unit=intl" if is_metric else ""
|
||||
self.devices = {}
|
||||
self.is_metric = is_metric
|
||||
_LOGGER.debug("Initial BloomSky device load")
|
||||
self.refresh_devices()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def refresh_devices(self):
|
||||
"""Use the API to retrieve a list of devices."""
|
||||
_LOGGER.debug("Fetching BloomSky update")
|
||||
response = requests.get(
|
||||
f"{self.API_URL}?{self._endpoint_argument}",
|
||||
headers={"Authorization": self._api_key},
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
raise RuntimeError("Invalid API_KEY")
|
||||
if response.status_code == HTTPStatus.METHOD_NOT_ALLOWED:
|
||||
_LOGGER.error("You have no bloomsky devices configured")
|
||||
return
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
_LOGGER.error("Invalid HTTP response: %s", response.status_code)
|
||||
return
|
||||
# Create dictionary keyed off of the device unique id
|
||||
self.devices.update({device["DeviceID"]: device for device in response.json()})
|
||||
@@ -1,68 +0,0 @@
|
||||
"""Support the binary sensors of a BloomSky weather station."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA as BINARY_SENSOR_PLATFORM_SCHEMA,
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
SENSOR_TYPES = {"Rain": BinarySensorDeviceClass.MOISTURE, "Night": None}
|
||||
|
||||
PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the available BloomSky weather binary sensors."""
|
||||
# Default needed in case of discovery
|
||||
if discovery_info is not None:
|
||||
return
|
||||
|
||||
sensors = config[CONF_MONITORED_CONDITIONS]
|
||||
bloomsky = hass.data[DOMAIN]
|
||||
|
||||
for device in bloomsky.devices.values():
|
||||
for variable in sensors:
|
||||
add_entities([BloomSkySensor(bloomsky, device, variable)], True)
|
||||
|
||||
|
||||
class BloomSkySensor(BinarySensorEntity):
|
||||
"""Representation of a single binary sensor in a BloomSky device."""
|
||||
|
||||
def __init__(self, bs, device, sensor_name):
|
||||
"""Initialize a BloomSky binary sensor."""
|
||||
self._bloomsky = bs
|
||||
self._device_id = device["DeviceID"]
|
||||
self._sensor_name = sensor_name
|
||||
self._attr_name = f"{device['DeviceName']} {sensor_name}"
|
||||
self._attr_unique_id = f"{self._device_id}-{sensor_name}"
|
||||
self._attr_device_class = SENSOR_TYPES.get(sensor_name)
|
||||
|
||||
def update(self) -> None:
|
||||
"""Request an update from the BloomSky API."""
|
||||
self._bloomsky.refresh_devices()
|
||||
|
||||
self._attr_is_on = self._bloomsky.devices[self._device_id]["Data"][
|
||||
self._sensor_name
|
||||
]
|
||||
@@ -1,67 +0,0 @@
|
||||
"""Support for a camera of a BloomSky weather station."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up access to BloomSky cameras."""
|
||||
if discovery_info is not None:
|
||||
return
|
||||
|
||||
bloomsky = hass.data[DOMAIN]
|
||||
|
||||
for device in bloomsky.devices.values():
|
||||
add_entities([BloomSkyCamera(bloomsky, device)])
|
||||
|
||||
|
||||
class BloomSkyCamera(Camera):
|
||||
"""Representation of the images published from the BloomSky's camera."""
|
||||
|
||||
def __init__(self, bs, device):
|
||||
"""Initialize access to the BloomSky camera images."""
|
||||
super().__init__()
|
||||
self._attr_name = device["DeviceName"]
|
||||
self._id = device["DeviceID"]
|
||||
self._bloomsky = bs
|
||||
self._url = ""
|
||||
self._last_url = ""
|
||||
# last_image will store images as they are downloaded so that the
|
||||
# frequent updates in home-assistant don't keep poking the server
|
||||
# to download the same image over and over.
|
||||
self._last_image = ""
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._attr_unique_id = self._id
|
||||
|
||||
def camera_image(
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Update the camera's image if it has changed."""
|
||||
try:
|
||||
self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"]
|
||||
self._bloomsky.refresh_devices()
|
||||
# If the URL hasn't changed then the image hasn't changed.
|
||||
if self._url != self._last_url:
|
||||
response = requests.get(self._url, timeout=10)
|
||||
self._last_url = self._url
|
||||
self._last_image = response.content
|
||||
except requests.exceptions.RequestException as error:
|
||||
self._logger.error("Error getting bloomsky image: %s", error)
|
||||
return None
|
||||
|
||||
return self._last_image
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"domain": "bloomsky",
|
||||
"name": "BloomSky",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bloomsky",
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user