mirror of
https://github.com/home-assistant/core.git
synced 2026-02-06 07:15:43 +01:00
Compare commits
538 Commits
tuya-energ
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01383ae84b | ||
|
|
22ab58077e | ||
|
|
4b666688c9 | ||
|
|
d118332366 | ||
|
|
9f32e0da14 | ||
|
|
1cef223a06 | ||
|
|
29da1233f3 | ||
|
|
a5b3d22058 | ||
|
|
d37e958a0b | ||
|
|
0498ac7364 | ||
|
|
67bdeb9945 | ||
|
|
a227307387 | ||
|
|
0e0309cabf | ||
|
|
fd2dfc83c6 | ||
|
|
9e736891c4 | ||
|
|
fbabf0dcb8 | ||
|
|
7128791152 | ||
|
|
94456b5bc3 | ||
|
|
2105c6b177 | ||
|
|
34156f79e8 | ||
|
|
bb1a2530f5 | ||
|
|
06613746f9 | ||
|
|
98ca948afe | ||
|
|
fa58fe5f4e | ||
|
|
46f230c487 | ||
|
|
13a987aba3 | ||
|
|
9cef323581 | ||
|
|
7ea7576188 | ||
|
|
f8abbfd42b | ||
|
|
5cd1821bc9 | ||
|
|
2ef7f26ffb | ||
|
|
184bea49e2 | ||
|
|
c853fb2068 | ||
|
|
79e0a93e48 | ||
|
|
3867c1d7d1 | ||
|
|
b9b6b050cc | ||
|
|
d960736b3d | ||
|
|
3e8923f105 | ||
|
|
17cca3e69d | ||
|
|
12714c489f | ||
|
|
f788d61b4a | ||
|
|
5c726af00b | ||
|
|
d1d207fbb2 | ||
|
|
6c7f8df7f7 | ||
|
|
6f8c9b1504 | ||
|
|
4f9aedbc84 | ||
|
|
afa0f572ce | ||
|
|
a6a1b9ddbd | ||
|
|
c1f5b4593f | ||
|
|
f1de4dc1cc | ||
|
|
4ae0d9a9c6 | ||
|
|
fcd0b579cf | ||
|
|
52fb0343e4 | ||
|
|
1050b4580a | ||
|
|
344c42172e | ||
|
|
93cc0fd7f1 | ||
|
|
05fe636b55 | ||
|
|
f22467d099 | ||
|
|
4bc3899b32 | ||
|
|
fc4d6bf5f1 | ||
|
|
8ed0672a8f | ||
|
|
282e347a1b | ||
|
|
1bfb02b440 | ||
|
|
71b03bd9ae | ||
|
|
cbd69822eb | ||
|
|
db900f4dd2 | ||
|
|
a707e695bc | ||
|
|
4feceac205 | ||
|
|
10c20faaca | ||
|
|
abcd512401 | ||
|
|
dee7a237ee | ||
|
|
3975eba12c | ||
|
|
ade91ebdab | ||
|
|
1bf194dd0f | ||
|
|
2eca8db8aa | ||
|
|
78415bc1ff | ||
|
|
e2469bcd0f | ||
|
|
54d64b7da2 | ||
|
|
d548f3d12f | ||
|
|
668995da73 | ||
|
|
9eeae8eac6 | ||
|
|
7e7056aa94 | ||
|
|
b633b8d271 | ||
|
|
45c7b9ccb8 | ||
|
|
3ad1a57dfc | ||
|
|
3cbe236a36 | ||
|
|
39816c1e8a | ||
|
|
5587dd43b9 | ||
|
|
715d1e4eb8 | ||
|
|
af172fb70d | ||
|
|
8c8bc104eb | ||
|
|
51b20fb5db | ||
|
|
445ba26667 | ||
|
|
886448f4ba | ||
|
|
ede4341ef3 | ||
|
|
fe363f32ec | ||
|
|
31562e7571 | ||
|
|
0bdb51e4ca | ||
|
|
67a5d7ac21 | ||
|
|
5e7f06c476 | ||
|
|
9a69852296 | ||
|
|
fdf8edf474 | ||
|
|
47e1a98bee | ||
|
|
2d8572b943 | ||
|
|
660cfdbd50 | ||
|
|
4208595da6 | ||
|
|
b6b2d2fc6f | ||
|
|
6c4c632848 | ||
|
|
78cf62176f | ||
|
|
df971c7a42 | ||
|
|
1fcabb7f2d | ||
|
|
9fb60c9ea2 | ||
|
|
9c11a4646f | ||
|
|
b036a78776 | ||
|
|
60bb3cb704 | ||
|
|
a722925b8e | ||
|
|
419c5de50e | ||
|
|
37faed565e | ||
|
|
622953e61f | ||
|
|
17926c3f6a | ||
|
|
48d85170c2 | ||
|
|
08d179c520 | ||
|
|
5752387da8 | ||
|
|
1ebde65f03 | ||
|
|
89f536e332 | ||
|
|
8784329333 | ||
|
|
d73538722d | ||
|
|
d49d3f0a2f | ||
|
|
8466dd4c2b | ||
|
|
6bb1e688c6 | ||
|
|
9bc1c4c4f3 | ||
|
|
a554cb8211 | ||
|
|
145d38403e | ||
|
|
10d4af5674 | ||
|
|
ed3b4d2de3 | ||
|
|
e66d324877 | ||
|
|
f7f18627a2 | ||
|
|
d18630020f | ||
|
|
a715ec318c | ||
|
|
0ef5a77dc9 | ||
|
|
b43abf83b8 | ||
|
|
84d28db3a7 | ||
|
|
74d99fa0be | ||
|
|
3ff0320ed8 | ||
|
|
16cb9e9785 | ||
|
|
d92279dfcb | ||
|
|
4b9d28d0e5 | ||
|
|
e6a60dfe50 | ||
|
|
d219056e9d | ||
|
|
6ff6b099b5 | ||
|
|
c5b9699098 | ||
|
|
6937bfdf67 | ||
|
|
39ee3fcfaa | ||
|
|
16cdfd05a0 | ||
|
|
f49d4787be | ||
|
|
2076700dc4 | ||
|
|
76c135913e | ||
|
|
c3534d5445 | ||
|
|
fc60b16d65 | ||
|
|
0443c93f77 | ||
|
|
0e770958ac | ||
|
|
2a54c71b6c | ||
|
|
50463291ab | ||
|
|
43cc34042a | ||
|
|
a02244ccda | ||
|
|
a739619121 | ||
|
|
5db97a5f1c | ||
|
|
804ba9c9cc | ||
|
|
5ecbcea946 | ||
|
|
11be2b6289 | ||
|
|
eefae0307b | ||
|
|
d397ee28ea | ||
|
|
02c821128e | ||
|
|
71dc15d45f | ||
|
|
1078387b22 | ||
|
|
35fab27d15 | ||
|
|
915dc7a908 | ||
|
|
e5a9738983 | ||
|
|
2ff73219a2 | ||
|
|
5dc1270ed1 | ||
|
|
9e95ad5a85 | ||
|
|
f97cf0e446 | ||
|
|
bd4fa0d5c2 | ||
|
|
f60d367184 | ||
|
|
6e231f2ec5 | ||
|
|
13ba2d2e47 | ||
|
|
ba4a163e24 | ||
|
|
b7db8684db | ||
|
|
a7595dc468 | ||
|
|
d2c8c3565b | ||
|
|
422d1031f4 | ||
|
|
c9a79cf100 | ||
|
|
c42d47a619 | ||
|
|
a26f871d32 | ||
|
|
d481c1bcc5 | ||
|
|
379e3596b4 | ||
|
|
423a7cdbba | ||
|
|
841fa48186 | ||
|
|
61e35157e3 | ||
|
|
87f655f56d | ||
|
|
692b8d0722 | ||
|
|
5f9f623c3f | ||
|
|
e595b6cd90 | ||
|
|
a748eebf3e | ||
|
|
6bdd544867 | ||
|
|
705eadf8ce | ||
|
|
b7c6e4eafc | ||
|
|
f4aba286fe | ||
|
|
5fa4f6de11 | ||
|
|
db1f045c42 | ||
|
|
eaba4817bd | ||
|
|
96cb2247df | ||
|
|
99fa7a1f52 | ||
|
|
e0ba928296 | ||
|
|
16fd5e8f1f | ||
|
|
201e95a417 | ||
|
|
dc01592991 | ||
|
|
c5fb2bd566 | ||
|
|
d03d996155 | ||
|
|
9618412a44 | ||
|
|
967e97661f | ||
|
|
b757312fe0 | ||
|
|
2ed8ec0bdf | ||
|
|
97f6e3741a | ||
|
|
c2d3244d26 | ||
|
|
eafeba792d | ||
|
|
c9318b6fbf | ||
|
|
99be382abf | ||
|
|
7cfcfca210 | ||
|
|
f29daccb19 | ||
|
|
be869fce6c | ||
|
|
7bb0414a39 | ||
|
|
3f8807d063 | ||
|
|
67642e6246 | ||
|
|
0d215597f3 | ||
|
|
f41bd2b582 | ||
|
|
5c9ec1911b | ||
|
|
1a0b7fe984 | ||
|
|
26ee25d7bb | ||
|
|
aabf52d3cf | ||
|
|
99fcb46a7e | ||
|
|
6580c5e5bf | ||
|
|
63e7d4dc08 | ||
|
|
cc6900d846 | ||
|
|
ca2ad22884 | ||
|
|
40944f0f2d | ||
|
|
91a3e488b1 | ||
|
|
9a1f517e6e | ||
|
|
9a5d4610f7 | ||
|
|
41c524fce4 | ||
|
|
5f9fa95554 | ||
|
|
6950be8ea9 | ||
|
|
c5a8bf64d0 | ||
|
|
a2b9a6e9df | ||
|
|
a0c567f0da | ||
|
|
c7feafdde6 | ||
|
|
e1e74b0aeb | ||
|
|
673411ef97 | ||
|
|
f7e5af7cb1 | ||
|
|
0ee56ce708 | ||
|
|
f93a176398 | ||
|
|
cd2394bc12 | ||
|
|
5c20b8eaff | ||
|
|
4bd499d3a6 | ||
|
|
8a53b94c5a | ||
|
|
d5aff326e3 | ||
|
|
22f66abbe7 | ||
|
|
f635228b1f | ||
|
|
4c708c143d | ||
|
|
c82c614bb9 | ||
|
|
20914dce67 | ||
|
|
5fc407d2f3 | ||
|
|
c7444d38a1 | ||
|
|
81f6136bda | ||
|
|
862d0ea49e | ||
|
|
f2fdfed241 | ||
|
|
15640049cb | ||
|
|
5c163434f8 | ||
|
|
e54c2ea55e | ||
|
|
1ec42693ab | ||
|
|
672864ae4f | ||
|
|
e54d7e42cb | ||
|
|
5d63fce015 | ||
|
|
190fe10eed | ||
|
|
ef410c1e2a | ||
|
|
5a712398e7 | ||
|
|
b1be3fe0da | ||
|
|
97a7ab011b | ||
|
|
694a3050b9 | ||
|
|
8164e65188 | ||
|
|
9af0d1eed4 | ||
|
|
72e6ca55ba | ||
|
|
0fb62a7e97 | ||
|
|
930eb70a8b | ||
|
|
462104fa68 | ||
|
|
d0c77d8a7e | ||
|
|
606780b20f | ||
|
|
8f465cf2ca | ||
|
|
4e29476dd9 | ||
|
|
b4328083be | ||
|
|
72ba59f559 | ||
|
|
826168b601 | ||
|
|
66f181992c | ||
|
|
336ef4c37b | ||
|
|
72e7bf7f9c | ||
|
|
acbdbc9be7 | ||
|
|
3551382f8d | ||
|
|
95014d7e6d | ||
|
|
dfe1990484 | ||
|
|
15ff5d0f74 | ||
|
|
1407f61a9c | ||
|
|
6107b794d6 | ||
|
|
7ab8ceab7e | ||
|
|
a4db6a9ebc | ||
|
|
12a2650b6b | ||
|
|
23da7ecedd | ||
|
|
8d9e7b0b26 | ||
|
|
9664047345 | ||
|
|
804fbf9cef | ||
|
|
e10fe074c9 | ||
|
|
7b0e21da74 | ||
|
|
29e142cf1e | ||
|
|
6b765ebabb | ||
|
|
899aa62697 | ||
|
|
a11efba405 | ||
|
|
78280dfc5a | ||
|
|
4220bab08a | ||
|
|
f7dcf8de15 | ||
|
|
7e32b50fee | ||
|
|
c875b75272 | ||
|
|
7368b9ca1d | ||
|
|
493e8c1a22 | ||
|
|
1b16b24550 | ||
|
|
7637300632 | ||
|
|
3369459d41 | ||
|
|
bdbce57217 | ||
|
|
8536472fe9 | ||
|
|
ad4fda7bb4 | ||
|
|
36e1b86952 | ||
|
|
0c9834e4ca | ||
|
|
360af74519 | ||
|
|
d099ac457d | ||
|
|
fc330ce165 | ||
|
|
b52dd5fc05 | ||
|
|
b517ce132f | ||
|
|
acec35846c | ||
|
|
af661898c2 | ||
|
|
e2f5a4849c | ||
|
|
399b7f6223 | ||
|
|
782f7af332 | ||
|
|
66af6565bf | ||
|
|
8a00aa8550 | ||
|
|
b07adc03d2 | ||
|
|
a978e3c199 | ||
|
|
bb3c977448 | ||
|
|
8057de408e | ||
|
|
0be4ee71e7 | ||
|
|
7ff5f14748 | ||
|
|
d5e58c817d | ||
|
|
8a08016fb9 | ||
|
|
d45ddd3762 | ||
|
|
0e98e8c893 | ||
|
|
84a09bec0e | ||
|
|
6fd27ec7ec | ||
|
|
91e2a318a5 | ||
|
|
1221c5bcad | ||
|
|
8e3befc301 | ||
|
|
2df62385f1 | ||
|
|
9f3b13dfa1 | ||
|
|
9c27e1233e | ||
|
|
825da95550 | ||
|
|
18bda2dbbe | ||
|
|
630a9b4896 | ||
|
|
e6399d2bfe | ||
|
|
4bae0d15ec | ||
|
|
760a75d1f1 | ||
|
|
c08912fc78 | ||
|
|
316d804336 | ||
|
|
d3658a52dd | ||
|
|
b3e42a1f07 | ||
|
|
dee07b25a2 | ||
|
|
f460bf36fe | ||
|
|
020d122799 | ||
|
|
699b4b12da | ||
|
|
3ec96f21d1 | ||
|
|
c6c5970864 | ||
|
|
570146c4a6 | ||
|
|
75b7f80f6c | ||
|
|
1c1a99e5ae | ||
|
|
0203f6e6f1 | ||
|
|
66612f97ec | ||
|
|
6d215c284c | ||
|
|
8e9e406341 | ||
|
|
b6772c4104 | ||
|
|
d6a830da1a | ||
|
|
2f7a895e28 | ||
|
|
5cb5b0eb45 | ||
|
|
33ae951030 | ||
|
|
1cb56216ba | ||
|
|
6409574ecf | ||
|
|
a94d39e493 | ||
|
|
fec008c589 | ||
|
|
358e58ea85 | ||
|
|
e8bbc9598f | ||
|
|
49e0c8e0bd | ||
|
|
0623da8aa9 | ||
|
|
8356524cf2 | ||
|
|
420123f1ff | ||
|
|
3ea3d88889 | ||
|
|
f88876a3c7 | ||
|
|
de834f9988 | ||
|
|
83ace00e14 | ||
|
|
1f163dfcbd | ||
|
|
057d24a227 | ||
|
|
9418217d38 | ||
|
|
181f89446f | ||
|
|
37b4bfc9fc | ||
|
|
b84022f88b | ||
|
|
a88ceada60 | ||
|
|
f72a70a549 | ||
|
|
70e84526cc | ||
|
|
1bb4c9d213 | ||
|
|
0b96aa8871 | ||
|
|
8645ef60ec | ||
|
|
a2e4980364 | ||
|
|
39ff57ecd2 | ||
|
|
f3025daa1f | ||
|
|
364ecc191e | ||
|
|
eaa1798443 | ||
|
|
11f713209d | ||
|
|
d96bc1b32e | ||
|
|
1022f422c8 | ||
|
|
3e21ac02fc | ||
|
|
f2a17a0aca | ||
|
|
0593bca476 | ||
|
|
c361185efb | ||
|
|
9882fe0eda | ||
|
|
bbb5ab448e | ||
|
|
9c07550f40 | ||
|
|
36b9234f26 | ||
|
|
2dc1981932 | ||
|
|
fda817cb1d | ||
|
|
8ccc9e407e | ||
|
|
bf07a79e3a | ||
|
|
6fd2d74539 | ||
|
|
8a91e07b97 | ||
|
|
c53b2d63d7 | ||
|
|
a8b7e1b5d9 | ||
|
|
9d90e3c7ce | ||
|
|
187aa52d92 | ||
|
|
0e08a6a69c | ||
|
|
be88a1f14a | ||
|
|
3794b4e1a1 | ||
|
|
46cc30e1f5 | ||
|
|
c261d39f99 | ||
|
|
f7bc7d3911 | ||
|
|
d880d305f4 | ||
|
|
19fe9c0f5e | ||
|
|
03eddfa142 | ||
|
|
d6a3189651 | ||
|
|
71f17f2cf1 | ||
|
|
6e2092b784 | ||
|
|
499fd131b0 | ||
|
|
9493240e9f | ||
|
|
ed70cacaa6 | ||
|
|
197e5203eb | ||
|
|
1f8a98609c | ||
|
|
69ee3a15b6 | ||
|
|
b13c2e3018 | ||
|
|
27b8274d3e | ||
|
|
cc6c506995 | ||
|
|
216bfeaa4a | ||
|
|
0781ac8450 | ||
|
|
5ec6a40ceb | ||
|
|
ec7a1fa266 | ||
|
|
729e530a6f | ||
|
|
c85c96a70e | ||
|
|
ff1898c334 | ||
|
|
68b4ad722d | ||
|
|
3c1bf41e5a | ||
|
|
9a03005d87 | ||
|
|
7e2878ec83 | ||
|
|
c6064f40d2 | ||
|
|
af5fe8e053 | ||
|
|
0ad692238f | ||
|
|
f220c0b8fe | ||
|
|
c33c834f5a | ||
|
|
0cf224fcb0 | ||
|
|
e50558c8fe | ||
|
|
99b43e74ea | ||
|
|
f4f25b3f96 | ||
|
|
e3c04d6a6b | ||
|
|
015df950f2 | ||
|
|
980c9bd9a0 | ||
|
|
b91abea97e | ||
|
|
6bb222bfc3 | ||
|
|
9ece2217eb | ||
|
|
d1fac72fe0 | ||
|
|
adb728baa1 | ||
|
|
e36432beed | ||
|
|
2830221820 | ||
|
|
4e0cbccc11 | ||
|
|
b341ae886b | ||
|
|
9913e1397e | ||
|
|
b1a048901f | ||
|
|
20c0c5f655 | ||
|
|
9fb1612dac | ||
|
|
6d064dfca0 | ||
|
|
8ee49bb2d3 | ||
|
|
2343ff3454 | ||
|
|
6224afc5d9 | ||
|
|
422dcb442d | ||
|
|
29fa9278a2 | ||
|
|
e478045ce2 | ||
|
|
0103c563d1 | ||
|
|
a265527ea7 | ||
|
|
bb45b23ba9 | ||
|
|
f8797ef06a | ||
|
|
253d0f0bf0 | ||
|
|
65a621e0b9 | ||
|
|
8727be4241 | ||
|
|
21e1ca7203 | ||
|
|
7c6a2d796a | ||
|
|
8cac2f9af2 | ||
|
|
c0d265375a | ||
|
|
98b7ffcbdd | ||
|
|
b18a8791ef | ||
|
|
b71add0769 | ||
|
|
2ee7155764 | ||
|
|
c5e1787e3b | ||
|
|
1bf9d72f7e | ||
|
|
19333a8056 | ||
|
|
664e97194a | ||
|
|
afb5330cfe | ||
|
|
42c816d2d4 | ||
|
|
ba0f3485ba | ||
|
|
a42caebf3e | ||
|
|
fad66d13f3 |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -80,7 +80,7 @@ If the code communicates with devices, web services, or third-party tools:
|
||||
Updated and included derived files by running: `python3 -m script.hassfest`.
|
||||
- [ ] New or updated dependencies have been added to `requirements_all.txt`.
|
||||
Updated by running `python3 -m script.gen_requirements_all`.
|
||||
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.
|
||||
- [ ] For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.
|
||||
|
||||
<!--
|
||||
This project is very active and we have a high turnover of pull requests.
|
||||
|
||||
4
.github/copilot-instructions.md
vendored
4
.github/copilot-instructions.md
vendored
@@ -192,6 +192,10 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Environment
|
||||
- **Local development (non-container)**: Activate the project venv before running commands: `source .venv/bin/activate`
|
||||
- **Dev container**: No activation needed, the environment is pre-configured
|
||||
|
||||
### Code Quality & Linting
|
||||
- **Run all linters on all files**: `prek run --all-files`
|
||||
- **Run linters on staged files only**: `prek run`
|
||||
|
||||
21
.github/workflows/builder.yml
vendored
21
.github/workflows/builder.yml
vendored
@@ -10,12 +10,12 @@ on:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: core
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
PIP_TIMEOUT: 60
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
# Base image version from https://github.com/home-assistant/docker
|
||||
BASE_IMAGE_VERSION: "2025.12.0"
|
||||
BASE_IMAGE_VERSION: "2026.01.0"
|
||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||
|
||||
jobs:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: OHF-Voice/intents-package
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -235,6 +235,7 @@ jobs:
|
||||
build-args: |
|
||||
BUILD_FROM=${{ steps.vars.outputs.base_image }}
|
||||
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
||||
outputs: type=image,push=true,compression=zstd,compression-level=9,force-compression=true,oci-mediatypes=true
|
||||
labels: |
|
||||
io.hass.arch=${{ matrix.arch }}
|
||||
io.hass.version=${{ needs.init.outputs.version }}
|
||||
@@ -287,7 +288,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -358,13 +359,13 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -522,7 +523,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -551,7 +552,7 @@ jobs:
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
||||
uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
|
||||
14
.github/workflows/ci.yaml
vendored
14
.github/workflows/ci.yaml
vendored
@@ -40,9 +40,9 @@ env:
|
||||
CACHE_VERSION: 2
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.2"
|
||||
DEFAULT_PYTHON: "3.13.11"
|
||||
ALL_PYTHON_VERSIONS: "['3.13.11', '3.14.2']"
|
||||
HA_SHORT_VERSION: "2026.3"
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
ALL_PYTHON_VERSIONS: "['3.14.2']"
|
||||
# 10.3 is the oldest supported version
|
||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||
# 10.6 is the current long-term-support
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
||||
echo "::add-matcher::.github/workflows/matchers/codespell.json"
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@9d6a3097e0c1865ecce00cfb89fe80f2ee91b547 # v1.0.12
|
||||
uses: j178/prek-action@564dda4cfa5e96aafdc4a5696c4bf7b46baae5ac # v1.1.0
|
||||
env:
|
||||
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
@@ -310,7 +310,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 actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: &actions-cache actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
key: &key-python-venv >-
|
||||
@@ -374,7 +374,7 @@ jobs:
|
||||
fi
|
||||
- name: Save apt cache
|
||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
uses: &actions-cache-save actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: &actions-cache-save actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: *path-apt-cache
|
||||
key: *key-apt-cache
|
||||
@@ -425,7 +425,7 @@ jobs:
|
||||
steps:
|
||||
- &cache-restore-apt
|
||||
name: Restore apt cache
|
||||
uses: &actions-cache-restore actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: &actions-cache-restore actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: *path-apt-cache
|
||||
fail-on-cache-miss: true
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
- "**strings.json"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
|
||||
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
@@ -17,7 +17,7 @@ on:
|
||||
- "script/gen_requirements_all.py"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.13
|
||||
3.14
|
||||
|
||||
@@ -53,6 +53,7 @@ homeassistant.components.air_quality.*
|
||||
homeassistant.components.airgradient.*
|
||||
homeassistant.components.airly.*
|
||||
homeassistant.components.airnow.*
|
||||
homeassistant.components.airobot.*
|
||||
homeassistant.components.airos.*
|
||||
homeassistant.components.airq.*
|
||||
homeassistant.components.airthings.*
|
||||
@@ -375,6 +376,7 @@ homeassistant.components.no_ip.*
|
||||
homeassistant.components.nordpool.*
|
||||
homeassistant.components.notify.*
|
||||
homeassistant.components.notion.*
|
||||
homeassistant.components.nrgkick.*
|
||||
homeassistant.components.ntfy.*
|
||||
homeassistant.components.number.*
|
||||
homeassistant.components.nut.*
|
||||
@@ -387,6 +389,7 @@ homeassistant.components.onkyo.*
|
||||
homeassistant.components.open_meteo.*
|
||||
homeassistant.components.open_router.*
|
||||
homeassistant.components.openai_conversation.*
|
||||
homeassistant.components.openevse.*
|
||||
homeassistant.components.openexchangerates.*
|
||||
homeassistant.components.opensky.*
|
||||
homeassistant.components.openuv.*
|
||||
@@ -432,6 +435,7 @@ homeassistant.components.raspberry_pi.*
|
||||
homeassistant.components.rdw.*
|
||||
homeassistant.components.recollect_waste.*
|
||||
homeassistant.components.recorder.*
|
||||
homeassistant.components.redgtech.*
|
||||
homeassistant.components.remember_the_milk.*
|
||||
homeassistant.components.remote.*
|
||||
homeassistant.components.remote_calendar.*
|
||||
|
||||
@@ -189,6 +189,10 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Environment
|
||||
- **Local development (non-container)**: Activate the project venv before running commands: `source .venv/bin/activate`
|
||||
- **Dev container**: No activation needed, the environment is pre-configured
|
||||
|
||||
### Code Quality & Linting
|
||||
- **Run all linters on all files**: `prek run --all-files`
|
||||
- **Run linters on staged files only**: `prek run`
|
||||
|
||||
17
CODEOWNERS
generated
17
CODEOWNERS
generated
@@ -288,6 +288,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/cloud/ @home-assistant/cloud
|
||||
/homeassistant/components/cloudflare/ @ludeeus @ctalkington
|
||||
/tests/components/cloudflare/ @ludeeus @ctalkington
|
||||
/homeassistant/components/cloudflare_r2/ @corrreia
|
||||
/tests/components/cloudflare_r2/ @corrreia
|
||||
/homeassistant/components/co2signal/ @jpbede @VIKTORVAV99
|
||||
/tests/components/co2signal/ @jpbede @VIKTORVAV99
|
||||
/homeassistant/components/coinbase/ @tombrien
|
||||
@@ -641,6 +643,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/gpsd/ @fabaff @jrieger
|
||||
/homeassistant/components/gree/ @cmroche
|
||||
/tests/components/gree/ @cmroche
|
||||
/homeassistant/components/green_planet_energy/ @petschni
|
||||
/tests/components/green_planet_energy/ @petschni
|
||||
/homeassistant/components/greeneye_monitor/ @jkeljo
|
||||
/tests/components/greeneye_monitor/ @jkeljo
|
||||
/homeassistant/components/group/ @home-assistant/core
|
||||
@@ -917,6 +921,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/libre_hardware_monitor/ @Sab44
|
||||
/homeassistant/components/lidarr/ @tkdrob
|
||||
/tests/components/lidarr/ @tkdrob
|
||||
/homeassistant/components/liebherr/ @mettolen
|
||||
/tests/components/liebherr/ @mettolen
|
||||
/homeassistant/components/lifx/ @Djelibeybi
|
||||
/tests/components/lifx/ @Djelibeybi
|
||||
/homeassistant/components/light/ @home-assistant/core
|
||||
@@ -1124,6 +1130,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/notify_events/ @matrozov @papajojo
|
||||
/homeassistant/components/notion/ @bachya
|
||||
/tests/components/notion/ @bachya
|
||||
/homeassistant/components/nrgkick/ @andijakl
|
||||
/tests/components/nrgkick/ @andijakl
|
||||
/homeassistant/components/nsw_fuel_station/ @nickw444
|
||||
/tests/components/nsw_fuel_station/ @nickw444
|
||||
/homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte
|
||||
@@ -1259,6 +1267,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/powerfox/ @klaasnicolaas
|
||||
/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson
|
||||
/tests/components/powerwall/ @bdraco @jrester @daniel-simpson
|
||||
/homeassistant/components/prana/ @prana-dev-official
|
||||
/tests/components/prana/ @prana-dev-official
|
||||
/homeassistant/components/private_ble_device/ @Jc2k
|
||||
/tests/components/private_ble_device/ @Jc2k
|
||||
/homeassistant/components/probe_plus/ @pantherale0
|
||||
@@ -1345,6 +1355,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/recorder/ @home-assistant/core
|
||||
/homeassistant/components/recovery_mode/ @home-assistant/core
|
||||
/tests/components/recovery_mode/ @home-assistant/core
|
||||
/homeassistant/components/redgtech/ @jonhsady @luan-nvg
|
||||
/tests/components/redgtech/ @jonhsady @luan-nvg
|
||||
/homeassistant/components/refoss/ @ashionky
|
||||
/tests/components/refoss/ @ashionky
|
||||
/homeassistant/components/rehlko/ @bdraco @peterager
|
||||
@@ -1722,6 +1734,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/twinkly/ @dr1rrb @Robbie1221 @Olen
|
||||
/homeassistant/components/twitch/ @joostlek
|
||||
/tests/components/twitch/ @joostlek
|
||||
/homeassistant/components/uhoo/ @getuhoo @joshsmonta
|
||||
/tests/components/uhoo/ @getuhoo @joshsmonta
|
||||
/homeassistant/components/ukraine_alarm/ @PaulAnnekov
|
||||
/tests/components/ukraine_alarm/ @PaulAnnekov
|
||||
/homeassistant/components/unifi/ @Kane610
|
||||
@@ -1809,6 +1823,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/water_heater/ @home-assistant/core
|
||||
/tests/components/water_heater/ @home-assistant/core
|
||||
/homeassistant/components/waterfurnace/ @sdague @masterkoppa
|
||||
/tests/components/waterfurnace/ @sdague @masterkoppa
|
||||
/homeassistant/components/watergate/ @adam-the-hero
|
||||
/tests/components/watergate/ @adam-the-hero
|
||||
/homeassistant/components/watson_tts/ @rutkai
|
||||
@@ -1867,6 +1882,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/worldclock/ @fabaff
|
||||
/homeassistant/components/ws66i/ @ssaenger
|
||||
/tests/components/ws66i/ @ssaenger
|
||||
/homeassistant/components/wsdot/ @ucodery
|
||||
/tests/components/wsdot/ @ucodery
|
||||
/homeassistant/components/wyoming/ @synesthesiam
|
||||
/tests/components/wyoming/ @synesthesiam
|
||||
/homeassistant/components/xbox/ @hunterjm @tr4nt0r
|
||||
|
||||
2
Dockerfile
generated
2
Dockerfile
generated
@@ -24,7 +24,7 @@ ENV \
|
||||
COPY rootfs /
|
||||
|
||||
# Add go2rtc binary
|
||||
COPY --from=ghcr.io/alexxit/go2rtc@sha256:f394f6329f5389a4c9a7fc54b09fdec9621bbb78bf7a672b973440bbdfb02241 /usr/local/bin/go2rtc /bin/go2rtc
|
||||
COPY --from=ghcr.io/alexxit/go2rtc@sha256:675c318b23c06fd862a61d262240c9a63436b4050d177ffc68a32710d9e05bae /usr/local/bin/go2rtc /bin/go2rtc
|
||||
|
||||
RUN \
|
||||
# Verify go2rtc can be executed
|
||||
|
||||
@@ -52,6 +52,9 @@ RUN --mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||||
--mount=type=bind,source=requirements_test_pre_commit.txt,target=requirements_test_pre_commit.txt \
|
||||
uv pip install -r requirements.txt -r requirements_test.txt
|
||||
|
||||
# Claude Code native install
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash
|
||||
|
||||
WORKDIR /workspaces
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
|
||||
5
homeassistant/brands/cloudflare.json
Normal file
5
homeassistant/brands/cloudflare.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "cloudflare",
|
||||
"name": "Cloudflare",
|
||||
"integrations": ["cloudflare", "cloudflare_r2"]
|
||||
}
|
||||
5
homeassistant/brands/heatit.json
Normal file
5
homeassistant/brands/heatit.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "heatit",
|
||||
"name": "Heatit",
|
||||
"iot_standards": ["zwave"]
|
||||
}
|
||||
5
homeassistant/brands/heiman.json
Normal file
5
homeassistant/brands/heiman.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "heiman",
|
||||
"name": "Heiman",
|
||||
"iot_standards": ["matter", "zigbee"]
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "leviton",
|
||||
"name": "Leviton",
|
||||
"integrations": ["decora_wifi"],
|
||||
"iot_standards": ["zwave"]
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_POLLING, DOMAIN, LOGGER
|
||||
from .const import CONF_POLLING, DOMAIN, DOMAIN_DATA, LOGGER
|
||||
from .services import async_setup_services
|
||||
|
||||
ATTR_DEVICE_NAME = "device_name"
|
||||
@@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||
raise ConfigEntryNotReady(f"Unable to connect to Abode: {ex}") from ex
|
||||
|
||||
hass.data[DOMAIN] = AbodeSystem(abode, polling)
|
||||
hass.data[DOMAIN_DATA] = AbodeSystem(abode, polling)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -113,11 +113,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop)
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN].abode.logout)
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN_DATA].abode.events.stop)
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN_DATA].abode.logout)
|
||||
|
||||
hass.data[DOMAIN].logout_listener()
|
||||
hass.data.pop(DOMAIN)
|
||||
if logout_listener := hass.data[DOMAIN_DATA].logout_listener:
|
||||
logout_listener()
|
||||
hass.data.pop(DOMAIN_DATA)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -127,16 +128,16 @@ async def setup_hass_events(hass: HomeAssistant) -> None:
|
||||
|
||||
def logout(event: Event) -> None:
|
||||
"""Logout of Abode."""
|
||||
if not hass.data[DOMAIN].polling:
|
||||
hass.data[DOMAIN].abode.events.stop()
|
||||
if not hass.data[DOMAIN_DATA].polling:
|
||||
hass.data[DOMAIN_DATA].abode.events.stop()
|
||||
|
||||
hass.data[DOMAIN].abode.logout()
|
||||
hass.data[DOMAIN_DATA].abode.logout()
|
||||
LOGGER.info("Logged out of Abode")
|
||||
|
||||
if not hass.data[DOMAIN].polling:
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.start)
|
||||
if not hass.data[DOMAIN_DATA].polling:
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN_DATA].abode.events.start)
|
||||
|
||||
hass.data[DOMAIN].logout_listener = hass.bus.async_listen_once(
|
||||
hass.data[DOMAIN_DATA].logout_listener = hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, logout
|
||||
)
|
||||
|
||||
@@ -178,6 +179,6 @@ def setup_abode_events(hass: HomeAssistant) -> None:
|
||||
]
|
||||
|
||||
for event in events:
|
||||
hass.data[DOMAIN].abode.events.add_event_callback(
|
||||
hass.data[DOMAIN_DATA].abode.events.add_event_callback(
|
||||
event, partial(event_callback, event)
|
||||
)
|
||||
|
||||
@@ -13,8 +13,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN_DATA
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
@@ -24,7 +23,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode alarm control panel device."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
async_add_entities(
|
||||
[AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))]
|
||||
)
|
||||
|
||||
@@ -15,8 +15,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN_DATA
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
@@ -26,7 +25,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode binary sensor devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
|
||||
device_types = [
|
||||
"connectivity",
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import DOMAIN_DATA, LOGGER
|
||||
from .entity import AbodeDevice
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
@@ -31,7 +31,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode camera devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
|
||||
async_add_entities(
|
||||
AbodeCamera(data, device, timeline.CAPTURE_IMAGE)
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
"""Constants for the Abode Security System component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AbodeSystem
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "abode"
|
||||
DOMAIN_DATA: HassKey[AbodeSystem] = HassKey(DOMAIN)
|
||||
ATTRIBUTION = "Data provided by goabode.com"
|
||||
|
||||
CONF_POLLING = "polling"
|
||||
|
||||
@@ -9,8 +9,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN_DATA
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
@@ -20,7 +19,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode cover devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
|
||||
async_add_entities(
|
||||
AbodeCover(data, device)
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import ATTRIBUTION, DOMAIN
|
||||
from .const import ATTRIBUTION, DOMAIN, DOMAIN_DATA
|
||||
|
||||
|
||||
class AbodeEntity(Entity):
|
||||
@@ -29,7 +29,7 @@ class AbodeEntity(Entity):
|
||||
self._update_connection_status,
|
||||
)
|
||||
|
||||
self.hass.data[DOMAIN].entity_ids.add(self.entity_id)
|
||||
self.hass.data[DOMAIN_DATA].entity_ids.add(self.entity_id)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Unsubscribe from Abode connection status updates."""
|
||||
|
||||
@@ -20,8 +20,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN_DATA
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
@@ -31,7 +30,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode light devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
|
||||
async_add_entities(
|
||||
AbodeLight(data, device)
|
||||
@@ -100,7 +99,7 @@ class AbodeLight(AbodeDevice, LightEntity):
|
||||
return _hs
|
||||
|
||||
@property
|
||||
def color_mode(self) -> str | None:
|
||||
def color_mode(self) -> ColorMode:
|
||||
"""Return the color mode of the light."""
|
||||
if self._device.is_dimmable and self._device.is_color_capable:
|
||||
if self.hs_color is not None:
|
||||
@@ -111,7 +110,7 @@ class AbodeLight(AbodeDevice, LightEntity):
|
||||
return ColorMode.ONOFF
|
||||
|
||||
@property
|
||||
def supported_color_modes(self) -> set[str] | None:
|
||||
def supported_color_modes(self) -> set[ColorMode]:
|
||||
"""Flag supported color modes."""
|
||||
if self._device.is_dimmable and self._device.is_color_capable:
|
||||
return {ColorMode.COLOR_TEMP, ColorMode.HS}
|
||||
|
||||
@@ -9,8 +9,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN_DATA
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
@@ -20,7 +19,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode lock devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
|
||||
async_add_entities(
|
||||
AbodeLock(data, device)
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN_DATA
|
||||
from .entity import AbodeDevice
|
||||
|
||||
ABODE_TEMPERATURE_UNIT_HA_UNIT = {
|
||||
@@ -66,7 +66,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode sensor devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
|
||||
async_add_entities(
|
||||
AbodeSensor(data, device, description)
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import DOMAIN, DOMAIN_DATA, LOGGER
|
||||
|
||||
SERVICE_SETTINGS = "change_setting"
|
||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||
@@ -35,7 +35,7 @@ def _change_setting(call: ServiceCall) -> None:
|
||||
value = call.data[ATTR_VALUE]
|
||||
|
||||
try:
|
||||
call.hass.data[DOMAIN].abode.set_setting(setting, value)
|
||||
call.hass.data[DOMAIN_DATA].abode.set_setting(setting, value)
|
||||
except AbodeException as ex:
|
||||
LOGGER.warning(ex)
|
||||
|
||||
@@ -46,7 +46,7 @@ def _capture_image(call: ServiceCall) -> None:
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in call.hass.data[DOMAIN].entity_ids
|
||||
for entity_id in call.hass.data[DOMAIN_DATA].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
@@ -61,7 +61,7 @@ def _trigger_automation(call: ServiceCall) -> None:
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in call.hass.data[DOMAIN].entity_ids
|
||||
for entity_id in call.hass.data[DOMAIN_DATA].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN_DATA
|
||||
from .entity import AbodeAutomation, AbodeDevice
|
||||
|
||||
DEVICE_TYPES = ["switch", "valve"]
|
||||
@@ -25,7 +24,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode switch devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
data = hass.data[DOMAIN_DATA]
|
||||
|
||||
entities: list[SwitchEntity] = [
|
||||
AbodeSwitch(data, device)
|
||||
|
||||
@@ -87,7 +87,7 @@ class AdaxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=data_schema,
|
||||
)
|
||||
|
||||
wifi_ssid = user_input[WIFI_SSID].replace(" ", "")
|
||||
wifi_ssid = user_input[WIFI_SSID]
|
||||
wifi_pswd = user_input[WIFI_PSWD].replace(" ", "")
|
||||
configurator = adax_local.AdaxConfig(wifi_ssid, wifi_pswd)
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_hassio(
|
||||
self, discovery_info: HassioServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Prepare configuration for a Hass.io AdGuard Home add-on.
|
||||
"""Prepare configuration for a Hass.io AdGuard Home app.
|
||||
|
||||
This flow is triggered by the discovery component.
|
||||
"""
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?",
|
||||
"title": "AdGuard Home via Home Assistant add-on"
|
||||
"description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the app: {addon}?",
|
||||
"title": "AdGuard Home via Home Assistant app"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
||||
@@ -7,10 +7,12 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, SERVER_URL
|
||||
from .services import async_setup_services
|
||||
|
||||
ATTRIBUTION = "ispyconnect.com"
|
||||
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
|
||||
@@ -19,6 +21,14 @@ PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
|
||||
|
||||
AgentDVRConfigEntry = ConfigEntry[Agent]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: AgentDVRConfigEntry
|
||||
|
||||
@@ -9,10 +9,7 @@ from homeassistant.components.camera import CameraEntityFeature
|
||||
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AgentDVRConfigEntry
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
|
||||
@@ -21,20 +18,6 @@ SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DEV_EN_ALT = "enable_alerts"
|
||||
_DEV_DS_ALT = "disable_alerts"
|
||||
_DEV_EN_REC = "start_recording"
|
||||
_DEV_DS_REC = "stop_recording"
|
||||
_DEV_SNAP = "snapshot"
|
||||
|
||||
CAMERA_SERVICES = {
|
||||
_DEV_EN_ALT: "async_enable_alerts",
|
||||
_DEV_DS_ALT: "async_disable_alerts",
|
||||
_DEV_EN_REC: "async_start_recording",
|
||||
_DEV_DS_REC: "async_stop_recording",
|
||||
_DEV_SNAP: "async_snapshot",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -57,10 +40,6 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(cameras)
|
||||
|
||||
platform = async_get_current_platform()
|
||||
for service, method in CAMERA_SERVICES.items():
|
||||
platform.async_register_entity_service(service, None, method)
|
||||
|
||||
|
||||
class AgentCamera(MjpegCamera):
|
||||
"""Representation of an Agent Device Stream."""
|
||||
|
||||
38
homeassistant/components/agent_dvr/services.py
Normal file
38
homeassistant/components/agent_dvr/services.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Services for Agent DVR."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import service
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_DEV_EN_ALT = "enable_alerts"
|
||||
_DEV_DS_ALT = "disable_alerts"
|
||||
_DEV_EN_REC = "start_recording"
|
||||
_DEV_DS_REC = "stop_recording"
|
||||
_DEV_SNAP = "snapshot"
|
||||
|
||||
CAMERA_SERVICES = {
|
||||
_DEV_EN_ALT: "async_enable_alerts",
|
||||
_DEV_DS_ALT: "async_disable_alerts",
|
||||
_DEV_EN_REC: "async_start_recording",
|
||||
_DEV_DS_REC: "async_stop_recording",
|
||||
_DEV_SNAP: "async_snapshot",
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
for service_name, method in CAMERA_SERVICES.items():
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
service_name,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema=None,
|
||||
func=method,
|
||||
)
|
||||
@@ -29,6 +29,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AirobotConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirobotDataUpdateCoordinator
|
||||
from .entity import AirobotEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
@@ -63,7 +64,7 @@ class AirobotClimate(AirobotEntity, ClimateEntity):
|
||||
_attr_min_temp = SETPOINT_TEMP_MIN
|
||||
_attr_max_temp = SETPOINT_TEMP_MAX
|
||||
|
||||
def __init__(self, coordinator) -> None:
|
||||
def __init__(self, coordinator: AirobotDataUpdateCoordinator) -> None:
|
||||
"""Initialize the climate entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = coordinator.data.status.device_id
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
@@ -60,11 +61,17 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> DeviceInf
|
||||
|
||||
try:
|
||||
# Try to fetch data to validate connection and authentication
|
||||
status = await client.get_statuses()
|
||||
settings = await client.get_settings()
|
||||
status, settings = await asyncio.gather(
|
||||
client.get_statuses(), client.get_settings()
|
||||
)
|
||||
except AirobotAuthError as err:
|
||||
raise InvalidAuth from err
|
||||
except (AirobotConnectionError, AirobotTimeoutError, AirobotError) as err:
|
||||
except (
|
||||
AirobotConnectionError,
|
||||
AirobotTimeoutError,
|
||||
AirobotError,
|
||||
TimeoutError,
|
||||
) as err:
|
||||
raise CannotConnect from err
|
||||
|
||||
# Use device name or device ID as title
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@@ -52,8 +53,10 @@ class AirobotDataUpdateCoordinator(DataUpdateCoordinator[AirobotData]):
|
||||
async def _async_update_data(self) -> AirobotData:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
status = await self.client.get_statuses()
|
||||
settings = await self.client.get_settings()
|
||||
status, settings = await asyncio.gather(
|
||||
self.client.get_statuses(),
|
||||
self.client.get_settings(),
|
||||
)
|
||||
except AirobotAuthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyairobotrest"],
|
||||
"quality_scale": "gold",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyairobotrest==0.3.0"]
|
||||
}
|
||||
|
||||
@@ -69,4 +69,4 @@ rules:
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
||||
strict-typing: done
|
||||
|
||||
@@ -28,6 +28,7 @@ from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util.variance import ignore_variance
|
||||
|
||||
from . import AirobotConfigEntry
|
||||
from .coordinator import AirobotDataUpdateCoordinator
|
||||
from .entity import AirobotEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -53,6 +54,7 @@ SENSOR_TYPES: tuple[AirobotSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
value_fn=lambda status: status.temp_air,
|
||||
),
|
||||
AirobotSensorEntityDescription(
|
||||
@@ -136,7 +138,7 @@ class AirobotSensor(AirobotEntity, SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator,
|
||||
coordinator: AirobotDataUpdateCoordinator,
|
||||
description: AirobotSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["airos==0.6.1"]
|
||||
"requirements": ["airos==0.6.3"]
|
||||
}
|
||||
|
||||
@@ -4,12 +4,24 @@
|
||||
"health_index": {
|
||||
"default": "mdi:heart-pulse"
|
||||
},
|
||||
"mold": {
|
||||
"default": "mdi:water-check"
|
||||
},
|
||||
"oxygen": {
|
||||
"default": "mdi:leaf"
|
||||
},
|
||||
"performance_index": {
|
||||
"default": "mdi:head-check"
|
||||
},
|
||||
"r32": {
|
||||
"default": "mdi:hvac"
|
||||
},
|
||||
"r454b": {
|
||||
"default": "mdi:hvac"
|
||||
},
|
||||
"r454c": {
|
||||
"default": "mdi:hvac"
|
||||
},
|
||||
"radon": {
|
||||
"default": "mdi:radioactive"
|
||||
},
|
||||
|
||||
@@ -219,6 +219,13 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value=lambda data: data.get("ch4_MIPEX"),
|
||||
),
|
||||
AirQEntityDescription(
|
||||
key="mold",
|
||||
translation_key="mold",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value=lambda data: data.get("mold"),
|
||||
),
|
||||
AirQEntityDescription(
|
||||
key="n2o",
|
||||
device_class=SensorDeviceClass.NITROUS_OXIDE,
|
||||
@@ -319,11 +326,25 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
|
||||
value=lambda data: data.get("c3h8_MIPEX"),
|
||||
),
|
||||
AirQEntityDescription(
|
||||
key="refigerant",
|
||||
translation_key="refigerant",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
key="r32",
|
||||
translation_key="r32",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value=lambda data: data.get("refigerant"),
|
||||
value=lambda data: data.get("r32"),
|
||||
),
|
||||
AirQEntityDescription(
|
||||
key="r454b",
|
||||
translation_key="r454b",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value=lambda data: data.get("r454b"),
|
||||
),
|
||||
AirQEntityDescription(
|
||||
key="r454c",
|
||||
translation_key="r454c",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value=lambda data: data.get("r454c"),
|
||||
),
|
||||
AirQEntityDescription(
|
||||
key="sih4",
|
||||
|
||||
@@ -101,6 +101,9 @@
|
||||
"methanethiol": {
|
||||
"name": "Methanethiol"
|
||||
},
|
||||
"mold": {
|
||||
"name": "Mold index"
|
||||
},
|
||||
"noise": {
|
||||
"name": "Noise"
|
||||
},
|
||||
@@ -116,12 +119,18 @@
|
||||
"propane": {
|
||||
"name": "Propane"
|
||||
},
|
||||
"r32": {
|
||||
"name": "Refrigerant R-32"
|
||||
},
|
||||
"r454b": {
|
||||
"name": "Refrigerant R-454B"
|
||||
},
|
||||
"r454c": {
|
||||
"name": "Refrigerant R-454C"
|
||||
},
|
||||
"radon": {
|
||||
"name": "Radon"
|
||||
},
|
||||
"refigerant": {
|
||||
"name": "Refrigerant"
|
||||
},
|
||||
"relative_pressure": {
|
||||
"name": "Relative pressure"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"connectivity_mode": {
|
||||
"default": "mdi:bluetooth-off",
|
||||
"state": {
|
||||
"bluetooth": "mdi:bluetooth",
|
||||
"not_configured": "mdi:alert-circle",
|
||||
"smartlink": "mdi:hub"
|
||||
}
|
||||
},
|
||||
"radon_1day_avg": {
|
||||
"default": "mdi:radioactive"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import dataclasses
|
||||
import logging
|
||||
|
||||
from airthings_ble import AirthingsDevice
|
||||
from airthings_ble import AirthingsConnectivityMode, AirthingsDevice
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -41,6 +41,12 @@ from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordina
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONNECTIVITY_MODE_MAP = {
|
||||
AirthingsConnectivityMode.BLE.value: "bluetooth",
|
||||
AirthingsConnectivityMode.SMARTLINK.value: "smartlink",
|
||||
AirthingsConnectivityMode.NOT_CONFIGURED.value: "not_configured",
|
||||
}
|
||||
|
||||
SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
||||
"radon_1day_avg": SensorEntityDescription(
|
||||
key="radon_1day_avg",
|
||||
@@ -129,6 +135,14 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"connectivity_mode": SensorEntityDescription(
|
||||
key="connectivity_mode",
|
||||
translation_key="connectivity_mode",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=list(CONNECTIVITY_MODE_MAP.values()),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
}
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -256,4 +270,12 @@ class AirthingsSensor(
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the value reported by the sensor."""
|
||||
return self.coordinator.data.sensors[self.entity_description.key]
|
||||
value = self.coordinator.data.sensors[self.entity_description.key]
|
||||
|
||||
# Map connectivity mode to enum values
|
||||
if self.entity_description.key == "connectivity_mode":
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
return CONNECTIVITY_MODE_MAP.get(value)
|
||||
|
||||
return value
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"ambient_noise": {
|
||||
"name": "Ambient noise"
|
||||
},
|
||||
"connectivity_mode": {
|
||||
"name": "Connectivity mode",
|
||||
"state": {
|
||||
"bluetooth": "Bluetooth",
|
||||
"not_configured": "Not configured",
|
||||
"smartlink": "SmartLink"
|
||||
}
|
||||
},
|
||||
"illuminance": {
|
||||
"name": "[%key:component::sensor::entity_component::illuminance::name%]"
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"oauth_implementation_unavailable": "[%key:common::config_flow::abort::oauth2_implementation_unavailable%]",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
},
|
||||
"services": {
|
||||
"alarm_arm_away": {
|
||||
"description": "Arms the alarm in the away mode.",
|
||||
"description": "Arms an alarm in the away mode.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]",
|
||||
@@ -176,7 +176,7 @@
|
||||
"name": "Arm away"
|
||||
},
|
||||
"alarm_arm_custom_bypass": {
|
||||
"description": "Arms the alarm while allowing to bypass a custom area.",
|
||||
"description": "Arms an alarm while allowing to bypass a custom area.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"description": "Code to arm the alarm.",
|
||||
@@ -186,7 +186,7 @@
|
||||
"name": "Arm with custom bypass"
|
||||
},
|
||||
"alarm_arm_home": {
|
||||
"description": "Arms the alarm in the home mode.",
|
||||
"description": "Arms an alarm in the home mode.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]",
|
||||
@@ -196,7 +196,7 @@
|
||||
"name": "Arm home"
|
||||
},
|
||||
"alarm_arm_night": {
|
||||
"description": "Arms the alarm in the night mode.",
|
||||
"description": "Arms an alarm in the night mode.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]",
|
||||
@@ -206,7 +206,7 @@
|
||||
"name": "Arm night"
|
||||
},
|
||||
"alarm_arm_vacation": {
|
||||
"description": "Arms the alarm in the vacation mode.",
|
||||
"description": "Arms an alarm in the vacation mode.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]",
|
||||
@@ -216,7 +216,7 @@
|
||||
"name": "Arm vacation"
|
||||
},
|
||||
"alarm_disarm": {
|
||||
"description": "Disarms the alarm.",
|
||||
"description": "Disarms an alarm.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"description": "Code to disarm the alarm.",
|
||||
@@ -226,7 +226,7 @@
|
||||
"name": "Disarm"
|
||||
},
|
||||
"alarm_trigger": {
|
||||
"description": "Triggers the alarm manually.",
|
||||
"description": "Triggers an alarm manually.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]",
|
||||
|
||||
@@ -18,12 +18,15 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_DEVICE_BAUD,
|
||||
CONF_DEVICE_PATH,
|
||||
DOMAIN,
|
||||
PROTOCOL_SERIAL,
|
||||
PROTOCOL_SOCKET,
|
||||
SIGNAL_PANEL_MESSAGE,
|
||||
@@ -32,9 +35,11 @@ from .const import (
|
||||
SIGNAL_ZONE_FAULT,
|
||||
SIGNAL_ZONE_RESTORE,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -54,6 +59,12 @@ class AlarmDecoderData:
|
||||
restart: bool
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AlarmDecoderConfigEntry
|
||||
) -> bool:
|
||||
|
||||
@@ -2,17 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
CodeFormat,
|
||||
)
|
||||
from homeassistant.const import ATTR_CODE
|
||||
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 AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -27,11 +23,6 @@ from .const import (
|
||||
)
|
||||
from .entity import AlarmDecoderEntity
|
||||
|
||||
SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime"
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = "alarm_keypress"
|
||||
ATTR_KEYPRESS = "keypress"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -50,23 +41,6 @@ async def async_setup_entry(
|
||||
)
|
||||
async_add_entities([entity])
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ALARM_TOGGLE_CHIME,
|
||||
{
|
||||
vol.Required(ATTR_CODE): cv.string,
|
||||
},
|
||||
"alarm_toggle_chime",
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ALARM_KEYPRESS,
|
||||
{
|
||||
vol.Required(ATTR_KEYPRESS): cv.string,
|
||||
},
|
||||
"alarm_keypress",
|
||||
)
|
||||
|
||||
|
||||
class AlarmDecoderAlarmPanel(AlarmDecoderEntity, AlarmControlPanelEntity):
|
||||
"""Representation of an AlarmDecoder-based alarm panel."""
|
||||
|
||||
46
homeassistant/components/alarmdecoder/services.py
Normal file
46
homeassistant/components/alarmdecoder/services.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Support for AlarmDecoder-based alarm control panels (Honeywell/DSC)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import ATTR_CODE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime"
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = "alarm_keypress"
|
||||
ATTR_KEYPRESS = "keypress"
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_ALARM_TOGGLE_CHIME,
|
||||
entity_domain=ALARM_CONTROL_PANEL_DOMAIN,
|
||||
schema={
|
||||
vol.Required(ATTR_CODE): cv.string,
|
||||
},
|
||||
func="alarm_toggle_chime",
|
||||
)
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_ALARM_KEYPRESS,
|
||||
entity_domain=ALARM_CONTROL_PANEL_DOMAIN,
|
||||
schema={
|
||||
vol.Required(ATTR_KEYPRESS): cv.string,
|
||||
},
|
||||
func="alarm_keypress",
|
||||
)
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==11.0.2"]
|
||||
"requirements": ["aioamazondevices==11.1.1"]
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ from homeassistant.helpers.typing import StateType
|
||||
from .const import CATEGORY_NOTIFICATIONS, CATEGORY_SENSORS
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
from .utils import async_remove_unsupported_notification_sensors
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -105,6 +106,9 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# Remove notification sensors from unsupported devices
|
||||
await async_remove_unsupported_notification_sensors(hass, coordinator)
|
||||
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
@@ -122,6 +126,7 @@ async def async_setup_entry(
|
||||
AmazonSensorEntity(coordinator, serial_num, notification_desc)
|
||||
for notification_desc in NOTIFICATIONS
|
||||
for serial_num in new_devices
|
||||
if coordinator.data[serial_num].notifications_supported
|
||||
]
|
||||
async_add_entities(sensors_list + notifications_list)
|
||||
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
"cannot_retrieve_data_with_error": {
|
||||
"message": "Error retrieving data: {error}"
|
||||
},
|
||||
"config_entry_not_found": {
|
||||
"message": "Config entry not found: {device_id}"
|
||||
},
|
||||
"device_serial_number_missing": {
|
||||
"message": "Device serial number missing: {device_id}"
|
||||
},
|
||||
|
||||
@@ -59,13 +59,15 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# Replace unique id for "DND" switch and remove from Speaker Group
|
||||
await async_update_unique_id(
|
||||
hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd"
|
||||
)
|
||||
# DND keys
|
||||
old_key = "do_not_disturb"
|
||||
new_key = "dnd"
|
||||
|
||||
# Remove DND switch from virtual groups
|
||||
await async_remove_dnd_from_virtual_group(hass, coordinator)
|
||||
# Remove old DND switch from virtual groups
|
||||
await async_remove_dnd_from_virtual_group(hass, coordinator, old_key)
|
||||
|
||||
# Replace unique id for DND switch
|
||||
await async_update_unique_id(hass, coordinator, SWITCH_DOMAIN, old_key, new_key)
|
||||
|
||||
known_devices: set[str] = set()
|
||||
|
||||
|
||||
@@ -5,8 +5,14 @@ from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
|
||||
from aioamazondevices.const.schedules import (
|
||||
NOTIFICATION_ALARM,
|
||||
NOTIFICATION_REMINDER,
|
||||
NOTIFICATION_TIMER,
|
||||
)
|
||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -48,7 +54,7 @@ def alexa_api_call[_T: AmazonEntity, **_P](
|
||||
async def async_update_unique_id(
|
||||
hass: HomeAssistant,
|
||||
coordinator: AmazonDevicesCoordinator,
|
||||
domain: str,
|
||||
platform: str,
|
||||
old_key: str,
|
||||
new_key: str,
|
||||
) -> None:
|
||||
@@ -57,7 +63,9 @@ async def async_update_unique_id(
|
||||
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-{old_key}"
|
||||
if entity_id := entity_registry.async_get_entity_id(domain, DOMAIN, unique_id):
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
DOMAIN, platform, unique_id
|
||||
):
|
||||
_LOGGER.debug("Updating unique_id for %s", entity_id)
|
||||
new_unique_id = unique_id.replace(old_key, new_key)
|
||||
|
||||
@@ -68,12 +76,13 @@ async def async_update_unique_id(
|
||||
async def async_remove_dnd_from_virtual_group(
|
||||
hass: HomeAssistant,
|
||||
coordinator: AmazonDevicesCoordinator,
|
||||
key: str,
|
||||
) -> None:
|
||||
"""Remove entity DND from virtual group."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-do_not_disturb"
|
||||
unique_id = f"{serial_num}-{key}"
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
DOMAIN, SWITCH_DOMAIN, unique_id
|
||||
)
|
||||
@@ -81,3 +90,27 @@ async def async_remove_dnd_from_virtual_group(
|
||||
if entity_id and is_group:
|
||||
entity_registry.async_remove(entity_id)
|
||||
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)
|
||||
|
||||
|
||||
async def async_remove_unsupported_notification_sensors(
|
||||
hass: HomeAssistant,
|
||||
coordinator: AmazonDevicesCoordinator,
|
||||
) -> None:
|
||||
"""Remove notification sensors from unsupported devices."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
for serial_num in coordinator.data:
|
||||
for notification_key in (
|
||||
NOTIFICATION_ALARM,
|
||||
NOTIFICATION_REMINDER,
|
||||
NOTIFICATION_TIMER,
|
||||
):
|
||||
unique_id = f"{serial_num}-{notification_key}"
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
DOMAIN, SENSOR_DOMAIN, unique_id=unique_id
|
||||
)
|
||||
is_unsupported = not coordinator.data[serial_num].notifications_supported
|
||||
|
||||
if entity_id and is_unsupported:
|
||||
entity_registry.async_remove(entity_id)
|
||||
_LOGGER.debug("Removed unsupported notification sensor %s", entity_id)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Callable
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
@@ -202,7 +202,7 @@ class AmcrestChecker(ApiWrapper):
|
||||
@asynccontextmanager
|
||||
async def async_stream_command(
|
||||
self, *args: Any, **kwargs: Any
|
||||
) -> AsyncIterator[httpx.Response]:
|
||||
) -> AsyncGenerator[httpx.Response]:
|
||||
"""amcrest.ApiWrapper.command wrapper to catch errors."""
|
||||
async with (
|
||||
self._async_command_wrapper(),
|
||||
@@ -211,7 +211,7 @@ class AmcrestChecker(ApiWrapper):
|
||||
yield ret
|
||||
|
||||
@asynccontextmanager
|
||||
async def _async_command_wrapper(self) -> AsyncIterator[None]:
|
||||
async def _async_command_wrapper(self) -> AsyncGenerator[None]:
|
||||
try:
|
||||
yield
|
||||
except LoginError as ex:
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components import labs, websocket_api
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -18,7 +18,13 @@ from .analytics import (
|
||||
EntityAnalyticsModifications,
|
||||
async_devices_payload,
|
||||
)
|
||||
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, PREFERENCE_SCHEMA
|
||||
from .const import (
|
||||
ATTR_ONBOARDED,
|
||||
ATTR_PREFERENCES,
|
||||
ATTR_SNAPSHOTS,
|
||||
DOMAIN,
|
||||
PREFERENCE_SCHEMA,
|
||||
)
|
||||
from .http import AnalyticsDevicesView
|
||||
|
||||
__all__ = [
|
||||
@@ -44,29 +50,55 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
|
||||
|
||||
LABS_SNAPSHOT_FEATURE = "snapshots"
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the analytics integration."""
|
||||
analytics_config = config.get(DOMAIN, {})
|
||||
|
||||
# For now we want to enable device analytics only if the url option
|
||||
# is explicitly listed in YAML.
|
||||
if CONF_SNAPSHOTS_URL in analytics_config:
|
||||
disable_snapshots = False
|
||||
await labs.async_update_preview_feature(
|
||||
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, enabled=True
|
||||
)
|
||||
snapshots_url = analytics_config[CONF_SNAPSHOTS_URL]
|
||||
else:
|
||||
disable_snapshots = True
|
||||
snapshots_url = None
|
||||
|
||||
analytics = Analytics(hass, snapshots_url, disable_snapshots)
|
||||
analytics = Analytics(hass, snapshots_url)
|
||||
|
||||
# Load stored data
|
||||
await analytics.load()
|
||||
|
||||
started = False
|
||||
|
||||
async def _async_handle_labs_update(
|
||||
event: Event[labs.EventLabsUpdatedData],
|
||||
) -> None:
|
||||
"""Handle labs feature toggle."""
|
||||
await analytics.save_preferences({ATTR_SNAPSHOTS: event.data["enabled"]})
|
||||
if started:
|
||||
await analytics.async_schedule()
|
||||
|
||||
@callback
|
||||
def _async_labs_event_filter(event_data: labs.EventLabsUpdatedData) -> bool:
|
||||
"""Filter labs events for this integration's snapshot feature."""
|
||||
return (
|
||||
event_data["domain"] == DOMAIN
|
||||
and event_data["preview_feature"] == LABS_SNAPSHOT_FEATURE
|
||||
)
|
||||
|
||||
async def start_schedule(_event: Event) -> None:
|
||||
"""Start the send schedule after the started event."""
|
||||
nonlocal started
|
||||
started = True
|
||||
await analytics.async_schedule()
|
||||
|
||||
hass.bus.async_listen(
|
||||
labs.EVENT_LABS_UPDATED,
|
||||
_async_handle_labs_update,
|
||||
event_filter=_async_labs_event_filter,
|
||||
)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_analytics)
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.components.energy import (
|
||||
DOMAIN as ENERGY_DOMAIN,
|
||||
is_configured as energy_is_configured,
|
||||
)
|
||||
from homeassistant.components.labs import async_is_preview_feature_enabled
|
||||
from homeassistant.components.recorder import (
|
||||
DOMAIN as RECORDER_DOMAIN,
|
||||
get_instance as get_recorder_instance,
|
||||
@@ -241,12 +242,10 @@ class Analytics:
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
snapshots_url: str | None = None,
|
||||
disable_snapshots: bool = False,
|
||||
) -> None:
|
||||
"""Initialize the Analytics class."""
|
||||
self._hass: HomeAssistant = hass
|
||||
self._snapshots_url = snapshots_url
|
||||
self._disable_snapshots = disable_snapshots
|
||||
|
||||
self._session = async_get_clientsession(hass)
|
||||
self._data = AnalyticsData(False, {})
|
||||
@@ -258,15 +257,13 @@ class Analytics:
|
||||
def preferences(self) -> dict:
|
||||
"""Return the current active preferences."""
|
||||
preferences = self._data.preferences
|
||||
result = {
|
||||
return {
|
||||
ATTR_BASE: preferences.get(ATTR_BASE, False),
|
||||
ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False),
|
||||
ATTR_USAGE: preferences.get(ATTR_USAGE, False),
|
||||
ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False),
|
||||
ATTR_SNAPSHOTS: preferences.get(ATTR_SNAPSHOTS, False),
|
||||
}
|
||||
if not self._disable_snapshots:
|
||||
result[ATTR_SNAPSHOTS] = preferences.get(ATTR_SNAPSHOTS, False)
|
||||
return result
|
||||
|
||||
@property
|
||||
def onboarded(self) -> bool:
|
||||
@@ -291,6 +288,11 @@ class Analytics:
|
||||
"""Return bool if a supervisor is present."""
|
||||
return is_hassio(self._hass)
|
||||
|
||||
@property
|
||||
def _snapshots_enabled(self) -> bool:
|
||||
"""Check if snapshots feature is enabled via labs."""
|
||||
return async_is_preview_feature_enabled(self._hass, DOMAIN, "snapshots")
|
||||
|
||||
async def load(self) -> None:
|
||||
"""Load preferences."""
|
||||
stored = await self._store.async_load()
|
||||
@@ -645,7 +647,10 @@ class Analytics:
|
||||
),
|
||||
)
|
||||
|
||||
if not self.preferences.get(ATTR_SNAPSHOTS, False) or self._disable_snapshots:
|
||||
if (
|
||||
not self.preferences.get(ATTR_SNAPSHOTS, False)
|
||||
or not self._snapshots_enabled
|
||||
):
|
||||
LOGGER.debug("Snapshot analytics not scheduled")
|
||||
if self._snapshot_scheduled:
|
||||
self._snapshot_scheduled()
|
||||
|
||||
@@ -7,5 +7,12 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"preview_features": {
|
||||
"snapshots": {
|
||||
"feedback_url": "https://forms.gle/GqvRmgmghSDco8M46",
|
||||
"learn_more_url": "https://www.home-assistant.io/blog/2026/02/02/about-device-database/",
|
||||
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new"
|
||||
}
|
||||
},
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
10
homeassistant/components/analytics/strings.json
Normal file
10
homeassistant/components/analytics/strings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"preview_features": {
|
||||
"snapshots": {
|
||||
"description": "We're creating the [Open Home Foundation Device Database](https://www.home-assistant.io/blog/2026/02/02/about-device-database/): a free, open source community-powered resource to help users find practical information about how smart home devices perform in real installations.\n\nYou can help us build it by opting in to share anonymized data about your devices. This data will only ever include device-specific details (like model or manufacturer) – never personally identifying information (like the names you assign).\n\nFind out how we process your data (should you choose to contribute) in our [Data Use Statement](https://www.openhomefoundation.org/device-database-data-use-statement).",
|
||||
"disable_confirmation": "Your data will no longer be shared with the Open Home Foundation's device database.",
|
||||
"enable_confirmation": "This feature is still in development and may change. The device database is being refined based on user feedback and is not yet complete.",
|
||||
"name": "Device database"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_TRACKED_INTEGRATIONS
|
||||
from .const import CONF_TRACKED_APPS, CONF_TRACKED_INTEGRATIONS
|
||||
from .coordinator import HomeassistantAnalyticsDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
@@ -59,6 +60,30 @@ async def async_setup_entry(
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, entry: AnalyticsInsightsConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate to a new version."""
|
||||
# Migration for switching add-ons to apps
|
||||
if entry.version < 2:
|
||||
ent_reg = er.async_get(hass)
|
||||
for entity_entry in er.async_entries_for_config_entry(ent_reg, entry.entry_id):
|
||||
if not entity_entry.unique_id.startswith("addon_"):
|
||||
continue
|
||||
|
||||
ent_reg.async_update_entity(
|
||||
entity_entry.entity_id,
|
||||
new_unique_id=entity_entry.unique_id.replace("addon_", "app_"),
|
||||
)
|
||||
|
||||
options = dict(entry.options)
|
||||
options[CONF_TRACKED_APPS] = options.pop("tracked_addons", [])
|
||||
|
||||
hass.config_entries.async_update_entry(entry, version=2, options=options)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: AnalyticsInsightsConfigEntry
|
||||
) -> bool:
|
||||
|
||||
@@ -26,7 +26,7 @@ from homeassistant.helpers.selector import (
|
||||
|
||||
from . import AnalyticsInsightsConfigEntry
|
||||
from .const import (
|
||||
CONF_TRACKED_ADDONS,
|
||||
CONF_TRACKED_APPS,
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
||||
CONF_TRACKED_INTEGRATIONS,
|
||||
DOMAIN,
|
||||
@@ -43,6 +43,8 @@ INTEGRATION_TYPES_WITHOUT_ANALYTICS = (
|
||||
class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Homeassistant Analytics."""
|
||||
|
||||
VERSION = 2
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
@@ -59,7 +61,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
if all(
|
||||
[
|
||||
not user_input.get(CONF_TRACKED_ADDONS),
|
||||
not user_input.get(CONF_TRACKED_APPS),
|
||||
not user_input.get(CONF_TRACKED_INTEGRATIONS),
|
||||
not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
|
||||
]
|
||||
@@ -70,7 +72,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
title="Home Assistant Analytics Insights",
|
||||
data={},
|
||||
options={
|
||||
CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
|
||||
CONF_TRACKED_APPS: user_input.get(CONF_TRACKED_APPS, []),
|
||||
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
||||
CONF_TRACKED_INTEGRATIONS, []
|
||||
),
|
||||
@@ -84,7 +86,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
session=async_get_clientsession(self.hass)
|
||||
)
|
||||
try:
|
||||
addons = await client.get_addons()
|
||||
apps = await client.get_addons()
|
||||
integrations = await client.get_integrations(Environment.NEXT)
|
||||
custom_integrations = await client.get_custom_integrations()
|
||||
except HomeassistantAnalyticsConnectionError:
|
||||
@@ -107,9 +109,9 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
|
||||
vol.Optional(CONF_TRACKED_APPS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=list(addons),
|
||||
options=list(apps),
|
||||
multiple=True,
|
||||
sort=True,
|
||||
)
|
||||
@@ -144,7 +146,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithReload):
|
||||
if user_input is not None:
|
||||
if all(
|
||||
[
|
||||
not user_input.get(CONF_TRACKED_ADDONS),
|
||||
not user_input.get(CONF_TRACKED_APPS),
|
||||
not user_input.get(CONF_TRACKED_INTEGRATIONS),
|
||||
not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
|
||||
]
|
||||
@@ -154,7 +156,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithReload):
|
||||
return self.async_create_entry(
|
||||
title="",
|
||||
data={
|
||||
CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
|
||||
CONF_TRACKED_APPS: user_input.get(CONF_TRACKED_APPS, []),
|
||||
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
||||
CONF_TRACKED_INTEGRATIONS, []
|
||||
),
|
||||
@@ -168,7 +170,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithReload):
|
||||
session=async_get_clientsession(self.hass)
|
||||
)
|
||||
try:
|
||||
addons = await client.get_addons()
|
||||
apps = await client.get_addons()
|
||||
integrations = await client.get_integrations(Environment.NEXT)
|
||||
custom_integrations = await client.get_custom_integrations()
|
||||
except HomeassistantAnalyticsConnectionError:
|
||||
@@ -189,9 +191,9 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithReload):
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
|
||||
vol.Optional(CONF_TRACKED_APPS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=list(addons),
|
||||
options=list(apps),
|
||||
multiple=True,
|
||||
sort=True,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import logging
|
||||
|
||||
DOMAIN = "analytics_insights"
|
||||
|
||||
CONF_TRACKED_ADDONS = "tracked_addons"
|
||||
CONF_TRACKED_APPS = "tracked_apps"
|
||||
CONF_TRACKED_INTEGRATIONS = "tracked_integrations"
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
CONF_TRACKED_ADDONS,
|
||||
CONF_TRACKED_APPS,
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
||||
CONF_TRACKED_INTEGRATIONS,
|
||||
DOMAIN,
|
||||
@@ -35,7 +35,7 @@ class AnalyticsData:
|
||||
|
||||
active_installations: int
|
||||
reports_integrations: int
|
||||
addons: dict[str, int]
|
||||
apps: dict[str, int]
|
||||
core_integrations: dict[str, int]
|
||||
custom_integrations: dict[str, int]
|
||||
|
||||
@@ -60,7 +60,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_apps = self.config_entry.options.get(CONF_TRACKED_APPS, [])
|
||||
self._tracked_integrations = self.config_entry.options[
|
||||
CONF_TRACKED_INTEGRATIONS
|
||||
]
|
||||
@@ -70,7 +70,9 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
||||
|
||||
async def _async_update_data(self) -> AnalyticsData:
|
||||
try:
|
||||
addons_data = await self._client.get_addons()
|
||||
apps_data = (
|
||||
await self._client.get_addons()
|
||||
) # Still add method name. Needs library update
|
||||
data = await self._client.get_current_analytics()
|
||||
custom_data = await self._client.get_custom_integrations()
|
||||
except HomeassistantAnalyticsConnectionError as err:
|
||||
@@ -79,9 +81,7 @@ 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
|
||||
}
|
||||
apps = {app: get_app_value(apps_data, app) for app in self._tracked_apps}
|
||||
core_integrations = {
|
||||
integration: data.integrations.get(integration, 0)
|
||||
for integration in self._tracked_integrations
|
||||
@@ -93,14 +93,14 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
||||
return AnalyticsData(
|
||||
data.active_installations,
|
||||
data.reports_integrations,
|
||||
addons,
|
||||
apps,
|
||||
core_integrations,
|
||||
custom_integrations,
|
||||
)
|
||||
|
||||
|
||||
def get_addon_value(data: dict[str, Addon], name_slug: str) -> int:
|
||||
"""Get addon value."""
|
||||
def get_app_value(data: dict[str, Addon], name_slug: str) -> int:
|
||||
"""Get app value."""
|
||||
if name_slug in data:
|
||||
return data[name_slug].total
|
||||
return 0
|
||||
|
||||
@@ -29,17 +29,17 @@ class AnalyticsSensorEntityDescription(SensorEntityDescription):
|
||||
value_fn: Callable[[AnalyticsData], StateType]
|
||||
|
||||
|
||||
def get_addon_entity_description(
|
||||
def get_app_entity_description(
|
||||
name_slug: str,
|
||||
) -> AnalyticsSensorEntityDescription:
|
||||
"""Get addon entity description."""
|
||||
"""Get app entity description."""
|
||||
return AnalyticsSensorEntityDescription(
|
||||
key=f"addon_{name_slug}_active_installations",
|
||||
translation_key="addons",
|
||||
key=f"app_{name_slug}_active_installations",
|
||||
translation_key="apps",
|
||||
name=name_slug,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement="active installations",
|
||||
value_fn=lambda data: data.addons.get(name_slug),
|
||||
value_fn=lambda data: data.apps.get(name_slug),
|
||||
)
|
||||
|
||||
|
||||
@@ -106,9 +106,9 @@ async def async_setup_entry(
|
||||
entities.extend(
|
||||
HomeassistantAnalyticsSensor(
|
||||
coordinator,
|
||||
get_addon_entity_description(addon_name_slug),
|
||||
get_app_entity_description(app_name_slug),
|
||||
)
|
||||
for addon_name_slug in coordinator.data.addons
|
||||
for app_name_slug in coordinator.data.apps
|
||||
)
|
||||
entities.extend(
|
||||
HomeassistantAnalyticsSensor(
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"tracked_addons": "Add-ons",
|
||||
"tracked_apps": "Apps",
|
||||
"tracked_custom_integrations": "Custom integrations",
|
||||
"tracked_integrations": "Integrations"
|
||||
},
|
||||
"data_description": {
|
||||
"tracked_addons": "Select the add-ons you want to track",
|
||||
"tracked_apps": "Select the apps you want to track",
|
||||
"tracked_custom_integrations": "Select the custom integrations you want to track",
|
||||
"tracked_integrations": "Select the integrations you want to track"
|
||||
}
|
||||
@@ -45,12 +45,12 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"tracked_addons": "[%key:component::analytics_insights::config::step::user::data::tracked_addons%]",
|
||||
"tracked_apps": "[%key:component::analytics_insights::config::step::user::data::tracked_apps%]",
|
||||
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]",
|
||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]"
|
||||
},
|
||||
"data_description": {
|
||||
"tracked_addons": "[%key:component::analytics_insights::config::step::user::data_description::tracked_addons%]",
|
||||
"tracked_apps": "[%key:component::analytics_insights::config::step::user::data_description::tracked_apps%]",
|
||||
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]",
|
||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]"
|
||||
}
|
||||
|
||||
@@ -14,10 +14,18 @@ from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
|
||||
from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
DATA_REPAIR_DEFER_RELOAD,
|
||||
DEFAULT_CONVERSATION_NAME,
|
||||
DEPRECATED_MODELS,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
@@ -27,6 +35,7 @@ type AnthropicConfigEntry = ConfigEntry[anthropic.AsyncClient]
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Anthropic."""
|
||||
hass.data.setdefault(DOMAIN, {}).setdefault(DATA_REPAIR_DEFER_RELOAD, set())
|
||||
await async_migrate_integration(hass)
|
||||
return True
|
||||
|
||||
@@ -50,6 +59,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||
|
||||
for subentry in entry.subentries.values():
|
||||
if (model := subentry.data.get(CONF_CHAT_MODEL)) and model.startswith(
|
||||
tuple(DEPRECATED_MODELS)
|
||||
):
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"model_deprecated",
|
||||
is_fixable=True,
|
||||
is_persistent=False,
|
||||
learn_more_url="https://platform.claude.com/docs/en/about-claude/model-deprecations",
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="model_deprecated",
|
||||
)
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -62,6 +87,11 @@ async def async_update_options(
|
||||
hass: HomeAssistant, entry: AnthropicConfigEntry
|
||||
) -> None:
|
||||
"""Update options."""
|
||||
defer_reload_entries: set[str] = hass.data.setdefault(DOMAIN, {}).setdefault(
|
||||
DATA_REPAIR_DEFER_RELOAD, set()
|
||||
)
|
||||
if entry.entry_id in defer_reload_entries:
|
||||
return
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
|
||||
@@ -92,6 +92,40 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
await client.models.list(timeout=10.0)
|
||||
|
||||
|
||||
async def get_model_list(client: anthropic.AsyncAnthropic) -> list[SelectOptionDict]:
|
||||
"""Get list of available models."""
|
||||
try:
|
||||
models = (await client.models.list()).data
|
||||
except anthropic.AnthropicError:
|
||||
models = []
|
||||
_LOGGER.debug("Available models: %s", models)
|
||||
model_options: list[SelectOptionDict] = []
|
||||
short_form = re.compile(r"[^\d]-\d$")
|
||||
for model_info in models:
|
||||
# Resolve alias from versioned model name:
|
||||
model_alias = (
|
||||
model_info.id[:-9]
|
||||
if model_info.id
|
||||
not in (
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-3-5-haiku-20241022",
|
||||
"claude-3-opus-20240229",
|
||||
)
|
||||
else model_info.id
|
||||
)
|
||||
if short_form.search(model_alias):
|
||||
model_alias += "-0"
|
||||
if model_alias.endswith(("haiku", "opus", "sonnet")):
|
||||
model_alias += "-latest"
|
||||
model_options.append(
|
||||
SelectOptionDict(
|
||||
label=model_info.display_name,
|
||||
value=model_alias,
|
||||
)
|
||||
)
|
||||
return model_options
|
||||
|
||||
|
||||
class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Anthropic."""
|
||||
|
||||
@@ -401,38 +435,13 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
|
||||
async def _get_model_list(self) -> list[SelectOptionDict]:
|
||||
"""Get list of available models."""
|
||||
try:
|
||||
client = await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
anthropic.AsyncAnthropic,
|
||||
api_key=self._get_entry().data[CONF_API_KEY],
|
||||
)
|
||||
client = await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
anthropic.AsyncAnthropic,
|
||||
api_key=self._get_entry().data[CONF_API_KEY],
|
||||
)
|
||||
models = (await client.models.list()).data
|
||||
except anthropic.AnthropicError:
|
||||
models = []
|
||||
_LOGGER.debug("Available models: %s", models)
|
||||
model_options: list[SelectOptionDict] = []
|
||||
short_form = re.compile(r"[^\d]-\d$")
|
||||
for model_info in models:
|
||||
# Resolve alias from versioned model name:
|
||||
model_alias = (
|
||||
model_info.id[:-9]
|
||||
if model_info.id
|
||||
not in ("claude-3-haiku-20240307", "claude-3-opus-20240229")
|
||||
else model_info.id
|
||||
)
|
||||
if short_form.search(model_alias):
|
||||
model_alias += "-0"
|
||||
if model_alias.endswith(("haiku", "opus", "sonnet")):
|
||||
model_alias += "-latest"
|
||||
model_options.append(
|
||||
SelectOptionDict(
|
||||
label=model_info.display_name,
|
||||
value=model_alias,
|
||||
)
|
||||
)
|
||||
return model_options
|
||||
)
|
||||
return await get_model_list(client)
|
||||
|
||||
async def _get_location_data(self) -> dict[str, str]:
|
||||
"""Get approximate location data of the user."""
|
||||
|
||||
@@ -22,8 +22,10 @@ CONF_WEB_SEARCH_REGION = "region"
|
||||
CONF_WEB_SEARCH_COUNTRY = "country"
|
||||
CONF_WEB_SEARCH_TIMEZONE = "timezone"
|
||||
|
||||
DATA_REPAIR_DEFER_RELOAD = "repair_defer_reload"
|
||||
|
||||
DEFAULT = {
|
||||
CONF_CHAT_MODEL: "claude-3-5-haiku-latest",
|
||||
CONF_CHAT_MODEL: "claude-haiku-4-5",
|
||||
CONF_MAX_TOKENS: 3000,
|
||||
CONF_TEMPERATURE: 1.0,
|
||||
CONF_THINKING_BUDGET: 0,
|
||||
@@ -46,3 +48,10 @@ WEB_SEARCH_UNSUPPORTED_MODELS = [
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
]
|
||||
|
||||
DEPRECATED_MODELS = [
|
||||
"claude-3-5-haiku",
|
||||
"claude-3-7-sonnet",
|
||||
"claude-3-5-sonnet",
|
||||
"claude-3-opus",
|
||||
]
|
||||
|
||||
@@ -600,6 +600,16 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
system = chat_log.content[0]
|
||||
if not isinstance(system, conversation.SystemContent):
|
||||
raise TypeError("First message must be a system message")
|
||||
|
||||
# System prompt with caching enabled
|
||||
system_prompt: list[TextBlockParam] = [
|
||||
TextBlockParam(
|
||||
type="text",
|
||||
text=system.content,
|
||||
cache_control={"type": "ephemeral"},
|
||||
)
|
||||
]
|
||||
|
||||
messages = _convert_content(chat_log.content[1:])
|
||||
|
||||
model = options.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL])
|
||||
@@ -608,7 +618,7 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
model=model,
|
||||
messages=messages,
|
||||
max_tokens=options.get(CONF_MAX_TOKENS, DEFAULT[CONF_MAX_TOKENS]),
|
||||
system=system.content,
|
||||
system=system_prompt,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
@@ -695,10 +705,6 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
type="auto",
|
||||
)
|
||||
|
||||
if isinstance(model_args["system"], str):
|
||||
model_args["system"] = [
|
||||
TextBlockParam(type="text", text=model_args["system"])
|
||||
]
|
||||
model_args["system"].append( # type: ignore[union-attr]
|
||||
TextBlockParam(
|
||||
type="text",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["anthropic==0.75.0"]
|
||||
"requirements": ["anthropic==0.78.0"]
|
||||
}
|
||||
|
||||
275
homeassistant/components/anthropic/repairs.py
Normal file
275
homeassistant/components/anthropic/repairs.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""Issue repair flow for Anthropic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from typing import cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.repairs import RepairsFlow
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigSubentry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||
|
||||
from .config_flow import get_model_list
|
||||
from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
DATA_REPAIR_DEFER_RELOAD,
|
||||
DEFAULT,
|
||||
DEPRECATED_MODELS,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
class ModelDeprecatedRepairFlow(RepairsFlow):
|
||||
"""Handler for an issue fixing flow."""
|
||||
|
||||
_subentry_iter: Iterator[tuple[str, str]] | None
|
||||
_current_entry_id: str | None
|
||||
_current_subentry_id: str | None
|
||||
_reload_pending: set[str]
|
||||
_pending_updates: dict[str, dict[str, str]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the flow."""
|
||||
super().__init__()
|
||||
self._subentry_iter = None
|
||||
self._current_entry_id = None
|
||||
self._current_subentry_id = None
|
||||
self._reload_pending = set()
|
||||
self._pending_updates = {}
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the first step of a fix flow."""
|
||||
previous_entry_id: str | None = None
|
||||
if user_input is not None:
|
||||
previous_entry_id = self._async_update_current_subentry(user_input)
|
||||
self._clear_current_target()
|
||||
|
||||
target = await self._async_next_target()
|
||||
next_entry_id = target[0].entry_id if target else None
|
||||
if previous_entry_id and previous_entry_id != next_entry_id:
|
||||
await self._async_apply_pending_updates(previous_entry_id)
|
||||
if target is None:
|
||||
await self._async_apply_all_pending_updates()
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
entry, subentry, model = target
|
||||
client = entry.runtime_data
|
||||
model_list = [
|
||||
model_option
|
||||
for model_option in await get_model_list(client)
|
||||
if not model_option["value"].startswith(tuple(DEPRECATED_MODELS))
|
||||
]
|
||||
|
||||
if "opus" in model:
|
||||
suggested_model = "claude-opus-4-5"
|
||||
elif "haiku" in model:
|
||||
suggested_model = "claude-haiku-4-5"
|
||||
elif "sonnet" in model:
|
||||
suggested_model = "claude-sonnet-4-5"
|
||||
else:
|
||||
suggested_model = cast(str, DEFAULT[CONF_CHAT_MODEL])
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_CHAT_MODEL,
|
||||
default=suggested_model,
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(options=model_list, custom_value=True)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=schema,
|
||||
description_placeholders={
|
||||
"entry_name": entry.title,
|
||||
"model": model,
|
||||
"subentry_name": subentry.title,
|
||||
"subentry_type": self._format_subentry_type(subentry.subentry_type),
|
||||
},
|
||||
)
|
||||
|
||||
def _iter_deprecated_subentries(self) -> Iterator[tuple[str, str]]:
|
||||
"""Yield entry/subentry pairs that use deprecated models."""
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
continue
|
||||
for subentry in entry.subentries.values():
|
||||
model = subentry.data.get(CONF_CHAT_MODEL)
|
||||
if model and model.startswith(tuple(DEPRECATED_MODELS)):
|
||||
yield entry.entry_id, subentry.subentry_id
|
||||
|
||||
async def _async_next_target(
|
||||
self,
|
||||
) -> tuple[ConfigEntry, ConfigSubentry, str] | None:
|
||||
"""Return the next deprecated subentry target."""
|
||||
if self._subentry_iter is None:
|
||||
self._subentry_iter = self._iter_deprecated_subentries()
|
||||
|
||||
while True:
|
||||
try:
|
||||
entry_id, subentry_id = next(self._subentry_iter)
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
entry = self.hass.config_entries.async_get_entry(entry_id)
|
||||
if entry is None:
|
||||
continue
|
||||
|
||||
subentry = entry.subentries.get(subentry_id)
|
||||
if subentry is None:
|
||||
continue
|
||||
|
||||
model = self._pending_model(entry_id, subentry_id)
|
||||
if model is None:
|
||||
model = subentry.data.get(CONF_CHAT_MODEL)
|
||||
if not model or not model.startswith(tuple(DEPRECATED_MODELS)):
|
||||
continue
|
||||
|
||||
self._current_entry_id = entry_id
|
||||
self._current_subentry_id = subentry_id
|
||||
return entry, subentry, model
|
||||
|
||||
def _async_update_current_subentry(self, user_input: dict[str, str]) -> str | None:
|
||||
"""Update the currently selected subentry."""
|
||||
if not self._current_entry_id or not self._current_subentry_id:
|
||||
return None
|
||||
|
||||
entry = self.hass.config_entries.async_get_entry(self._current_entry_id)
|
||||
if entry is None:
|
||||
return None
|
||||
|
||||
subentry = entry.subentries.get(self._current_subentry_id)
|
||||
if subentry is None:
|
||||
return None
|
||||
|
||||
updated_data = {
|
||||
**subentry.data,
|
||||
CONF_CHAT_MODEL: user_input[CONF_CHAT_MODEL],
|
||||
}
|
||||
if updated_data == subentry.data:
|
||||
return entry.entry_id
|
||||
self._queue_pending_update(
|
||||
entry.entry_id,
|
||||
subentry.subentry_id,
|
||||
updated_data[CONF_CHAT_MODEL],
|
||||
)
|
||||
return entry.entry_id
|
||||
|
||||
def _clear_current_target(self) -> None:
|
||||
"""Clear current target tracking."""
|
||||
self._current_entry_id = None
|
||||
self._current_subentry_id = None
|
||||
|
||||
def _format_subentry_type(self, subentry_type: str) -> str:
|
||||
"""Return a user-friendly subentry type label."""
|
||||
if subentry_type == "conversation":
|
||||
return "Conversation agent"
|
||||
if subentry_type in ("ai_task", "ai_task_data"):
|
||||
return "AI task"
|
||||
return subentry_type
|
||||
|
||||
def _queue_pending_update(
|
||||
self, entry_id: str, subentry_id: str, model: str
|
||||
) -> None:
|
||||
"""Store a pending model update for a subentry."""
|
||||
self._pending_updates.setdefault(entry_id, {})[subentry_id] = model
|
||||
|
||||
def _pending_model(self, entry_id: str, subentry_id: str) -> str | None:
|
||||
"""Return a pending model update if one exists."""
|
||||
return self._pending_updates.get(entry_id, {}).get(subentry_id)
|
||||
|
||||
def _mark_entry_for_reload(self, entry_id: str) -> None:
|
||||
"""Prevent reload until repairs are complete for the entry."""
|
||||
self._reload_pending.add(entry_id)
|
||||
defer_reload_entries: set[str] = self.hass.data.setdefault(
|
||||
DOMAIN, {}
|
||||
).setdefault(DATA_REPAIR_DEFER_RELOAD, set())
|
||||
defer_reload_entries.add(entry_id)
|
||||
|
||||
async def _async_reload_entry(self, entry_id: str) -> None:
|
||||
"""Reload an entry once all repairs are completed."""
|
||||
if entry_id not in self._reload_pending:
|
||||
return
|
||||
|
||||
entry = self.hass.config_entries.async_get_entry(entry_id)
|
||||
if entry is not None and entry.state is not ConfigEntryState.LOADED:
|
||||
self._clear_defer_reload(entry_id)
|
||||
self._reload_pending.discard(entry_id)
|
||||
return
|
||||
|
||||
if entry is not None:
|
||||
await self.hass.config_entries.async_reload(entry_id)
|
||||
|
||||
self._clear_defer_reload(entry_id)
|
||||
self._reload_pending.discard(entry_id)
|
||||
|
||||
def _clear_defer_reload(self, entry_id: str) -> None:
|
||||
"""Remove entry from the deferred reload set."""
|
||||
defer_reload_entries: set[str] = self.hass.data.setdefault(
|
||||
DOMAIN, {}
|
||||
).setdefault(DATA_REPAIR_DEFER_RELOAD, set())
|
||||
defer_reload_entries.discard(entry_id)
|
||||
|
||||
async def _async_apply_pending_updates(self, entry_id: str) -> None:
|
||||
"""Apply pending subentry updates for a single entry."""
|
||||
updates = self._pending_updates.pop(entry_id, None)
|
||||
if not updates:
|
||||
return
|
||||
|
||||
entry = self.hass.config_entries.async_get_entry(entry_id)
|
||||
if entry is None or entry.state is not ConfigEntryState.LOADED:
|
||||
return
|
||||
|
||||
changed = False
|
||||
for subentry_id, model in updates.items():
|
||||
subentry = entry.subentries.get(subentry_id)
|
||||
if subentry is None:
|
||||
continue
|
||||
|
||||
updated_data = {
|
||||
**subentry.data,
|
||||
CONF_CHAT_MODEL: model,
|
||||
}
|
||||
if updated_data == subentry.data:
|
||||
continue
|
||||
|
||||
if not changed:
|
||||
self._mark_entry_for_reload(entry_id)
|
||||
changed = True
|
||||
|
||||
self.hass.config_entries.async_update_subentry(
|
||||
entry,
|
||||
subentry,
|
||||
data=updated_data,
|
||||
)
|
||||
|
||||
if not changed:
|
||||
return
|
||||
|
||||
await self._async_reload_entry(entry_id)
|
||||
|
||||
async def _async_apply_all_pending_updates(self) -> None:
|
||||
"""Apply all pending updates across entries."""
|
||||
for entry_id in list(self._pending_updates):
|
||||
await self._async_apply_pending_updates(entry_id)
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
data: dict[str, str | int | float | None] | None,
|
||||
) -> RepairsFlow:
|
||||
"""Create flow."""
|
||||
if issue_id == "model_deprecated":
|
||||
return ModelDeprecatedRepairFlow()
|
||||
raise HomeAssistantError("Unknown issue ID")
|
||||
@@ -109,5 +109,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"model_deprecated": {
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"chat_model": "[%key:common::generic::model%]"
|
||||
},
|
||||
"description": "You are updating {subentry_name} ({subentry_type}) in {entry_name}. The current model {model} is deprecated. Select a supported model to continue.",
|
||||
"title": "Update model"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Model deprecated"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["py-aosmith==1.0.15"]
|
||||
"requirements": ["py-aosmith==1.0.16"]
|
||||
}
|
||||
|
||||
@@ -540,7 +540,17 @@ class APCUPSdSensor(APCUPSdEntity, SensorEntity):
|
||||
data = self.coordinator.data[key]
|
||||
|
||||
if self.entity_description.device_class == SensorDeviceClass.TIMESTAMP:
|
||||
self._attr_native_value = dateutil.parser.parse(data)
|
||||
# The date could be "N/A" for certain fields (e.g., XOFFBATT), indicating there is no value yet.
|
||||
if data == "N/A":
|
||||
self._attr_native_value = None
|
||||
return
|
||||
|
||||
try:
|
||||
self._attr_native_value = dateutil.parser.parse(data)
|
||||
except (dateutil.parser.ParserError, OverflowError):
|
||||
# If parsing fails we should mark it as unknown, with a log for further debugging.
|
||||
_LOGGER.warning('Failed to parse date for %s: "%s"', key, data)
|
||||
self._attr_native_value = None
|
||||
return
|
||||
|
||||
self._attr_native_value, inferred_unit = infer_unit(data)
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pyatv.const import KeyboardFocusState
|
||||
from pyatv.const import FeatureName, FeatureState, KeyboardFocusState
|
||||
from pyatv.interface import AppleTV, KeyboardListener
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AppleTvConfigEntry
|
||||
from . import SIGNAL_CONNECTED, AppleTvConfigEntry
|
||||
from .entity import AppleTVEntity
|
||||
|
||||
|
||||
@@ -21,10 +22,22 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Load Apple TV binary sensor based on a config entry."""
|
||||
# apple_tv config entries always have a unique id
|
||||
assert config_entry.unique_id is not None
|
||||
name: str = config_entry.data[CONF_NAME]
|
||||
manager = config_entry.runtime_data
|
||||
async_add_entities([AppleTVKeyboardFocused(name, config_entry.unique_id, manager)])
|
||||
cb: CALLBACK_TYPE
|
||||
|
||||
def setup_entities(atv: AppleTV) -> None:
|
||||
if atv.features.in_state(FeatureState.Available, FeatureName.TextFocusState):
|
||||
assert config_entry.unique_id is not None
|
||||
name: str = config_entry.data[CONF_NAME]
|
||||
async_add_entities(
|
||||
[AppleTVKeyboardFocused(name, config_entry.unique_id, manager)]
|
||||
)
|
||||
cb()
|
||||
|
||||
cb = async_dispatcher_connect(
|
||||
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
|
||||
)
|
||||
config_entry.async_on_unload(cb)
|
||||
|
||||
|
||||
class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener):
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"oauth_implementation_unavailable": "[%key:common::config_flow::abort::oauth2_implementation_unavailable%]",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"reauth_invalid_user": "Reauthenticate must use the same account.",
|
||||
|
||||
@@ -7,7 +7,7 @@ import asyncio
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Literal, Protocol, cast
|
||||
from typing import Any, Protocol, cast
|
||||
|
||||
from propcache.api import cached_property
|
||||
import voluptuous as vol
|
||||
@@ -25,18 +25,11 @@ from homeassistant.const import (
|
||||
CONF_ACTIONS,
|
||||
CONF_ALIAS,
|
||||
CONF_CONDITIONS,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_EVENT_DATA,
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_OPTIONS,
|
||||
CONF_PATH,
|
||||
CONF_PLATFORM,
|
||||
CONF_TARGET,
|
||||
CONF_TRIGGERS,
|
||||
CONF_VARIABLES,
|
||||
CONF_ZONE,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
SERVICE_RELOAD,
|
||||
SERVICE_TOGGLE,
|
||||
@@ -53,10 +46,13 @@ from homeassistant.core import (
|
||||
ServiceCall,
|
||||
callback,
|
||||
split_entity_id,
|
||||
valid_entity_id,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError
|
||||
from homeassistant.helpers import condition as condition_helper, config_validation as cv
|
||||
from homeassistant.helpers import (
|
||||
condition as condition_helper,
|
||||
config_validation as cv,
|
||||
trigger as trigger_helper,
|
||||
)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
@@ -86,7 +82,6 @@ from homeassistant.helpers.trace import (
|
||||
trace_get,
|
||||
trace_path,
|
||||
)
|
||||
from homeassistant.helpers.trigger import async_initialize_triggers
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.dt import parse_datetime
|
||||
@@ -125,10 +120,18 @@ NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
|
||||
_EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"alarm_control_panel",
|
||||
"assist_satellite",
|
||||
"climate",
|
||||
"device_tracker",
|
||||
"fan",
|
||||
"humidifier",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"person",
|
||||
"siren",
|
||||
"switch",
|
||||
"vacuum",
|
||||
}
|
||||
|
||||
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
@@ -610,7 +613,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_LABEL_ID))
|
||||
referenced |= set(trigger_helper.async_extract_targets(conf, ATTR_LABEL_ID))
|
||||
return referenced
|
||||
|
||||
@cached_property
|
||||
@@ -625,7 +628,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_FLOOR_ID))
|
||||
referenced |= set(trigger_helper.async_extract_targets(conf, ATTR_FLOOR_ID))
|
||||
return referenced
|
||||
|
||||
@cached_property
|
||||
@@ -638,7 +641,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
referenced |= condition_helper.async_extract_targets(conf, ATTR_AREA_ID)
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_AREA_ID))
|
||||
referenced |= set(trigger_helper.async_extract_targets(conf, ATTR_AREA_ID))
|
||||
return referenced
|
||||
|
||||
@property
|
||||
@@ -658,7 +661,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
referenced |= condition_helper.async_extract_devices(conf)
|
||||
|
||||
for conf in self._trigger_config:
|
||||
referenced |= set(_trigger_extract_devices(conf))
|
||||
referenced |= set(trigger_helper.async_extract_devices(conf))
|
||||
|
||||
return referenced
|
||||
|
||||
@@ -672,7 +675,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
referenced |= condition_helper.async_extract_entities(conf)
|
||||
|
||||
for conf in self._trigger_config:
|
||||
for entity_id in _trigger_extract_entities(conf):
|
||||
for entity_id in trigger_helper.async_extract_entities(conf):
|
||||
referenced.add(entity_id)
|
||||
|
||||
return referenced
|
||||
@@ -946,7 +949,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
self._logger.error("Error rendering trigger variables: %s", err)
|
||||
return None
|
||||
|
||||
return await async_initialize_triggers(
|
||||
return await trigger_helper.async_initialize_triggers(
|
||||
self.hass,
|
||||
self._trigger_config,
|
||||
self._async_trigger_if_enabled,
|
||||
@@ -1230,78 +1233,6 @@ async def _async_process_if(
|
||||
return result
|
||||
|
||||
|
||||
@callback
|
||||
def _trigger_extract_devices(trigger_conf: dict) -> list[str]:
|
||||
"""Extract devices from a trigger config."""
|
||||
if trigger_conf[CONF_PLATFORM] == "device":
|
||||
return [trigger_conf[CONF_DEVICE_ID]]
|
||||
|
||||
if (
|
||||
trigger_conf[CONF_PLATFORM] == "event"
|
||||
and CONF_EVENT_DATA in trigger_conf
|
||||
and CONF_DEVICE_ID in trigger_conf[CONF_EVENT_DATA]
|
||||
and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID], str)
|
||||
):
|
||||
return [trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID]]
|
||||
|
||||
if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf:
|
||||
return trigger_conf[CONF_DEVICE_ID] # type: ignore[no-any-return]
|
||||
|
||||
if target_devices := _get_targets_from_trigger_config(trigger_conf, CONF_DEVICE_ID):
|
||||
return target_devices
|
||||
|
||||
return []
|
||||
|
||||
|
||||
@callback
|
||||
def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
|
||||
"""Extract entities from a trigger config."""
|
||||
if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"):
|
||||
return trigger_conf[CONF_ENTITY_ID] # type: ignore[no-any-return]
|
||||
|
||||
if trigger_conf[CONF_PLATFORM] == "calendar":
|
||||
return [trigger_conf[CONF_OPTIONS][CONF_ENTITY_ID]]
|
||||
|
||||
if trigger_conf[CONF_PLATFORM] == "zone":
|
||||
return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] # type: ignore[no-any-return]
|
||||
|
||||
if trigger_conf[CONF_PLATFORM] == "geo_location":
|
||||
return [trigger_conf[CONF_ZONE]]
|
||||
|
||||
if trigger_conf[CONF_PLATFORM] == "sun":
|
||||
return ["sun.sun"]
|
||||
|
||||
if (
|
||||
trigger_conf[CONF_PLATFORM] == "event"
|
||||
and CONF_EVENT_DATA in trigger_conf
|
||||
and CONF_ENTITY_ID in trigger_conf[CONF_EVENT_DATA]
|
||||
and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID], str)
|
||||
and valid_entity_id(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID])
|
||||
):
|
||||
return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]]
|
||||
|
||||
if target_entities := _get_targets_from_trigger_config(
|
||||
trigger_conf, CONF_ENTITY_ID
|
||||
):
|
||||
return target_entities
|
||||
|
||||
return []
|
||||
|
||||
|
||||
@callback
|
||||
def _get_targets_from_trigger_config(
|
||||
config: dict,
|
||||
target: Literal["entity_id", "device_id", "area_id", "floor_id", "label_id"],
|
||||
) -> list[str]:
|
||||
"""Extract targets from a target config."""
|
||||
if not (target_conf := config.get(CONF_TARGET)):
|
||||
return []
|
||||
if not (targets := target_conf.get(target)):
|
||||
return []
|
||||
|
||||
return [targets] if isinstance(targets, str) else targets
|
||||
|
||||
|
||||
@websocket_api.websocket_command({"type": "automation/config", "entity_id": str})
|
||||
def websocket_config(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -52,7 +52,7 @@ from .const import (
|
||||
from .errors import AuthenticationRequired, CannotConnect
|
||||
from .hub import AxisHub, get_axis_api
|
||||
|
||||
AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"}
|
||||
AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f", "e8:27:25"}
|
||||
DEFAULT_PORT = 443
|
||||
DEFAULT_PROTOCOL = "https"
|
||||
PROTOCOL_CHOICES = ["https", "http"]
|
||||
|
||||
@@ -26,6 +26,7 @@ EXCLUDE_FROM_BACKUP = [
|
||||
"tmp_backups/*.tar",
|
||||
"OZW_Log.txt",
|
||||
"tts/*",
|
||||
".cache/*",
|
||||
]
|
||||
|
||||
EXCLUDE_DATABASE_FROM_BACKUP = [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for Baidu speech service."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aip import AipSpeech
|
||||
import voluptuous as vol
|
||||
@@ -9,6 +10,7 @@ from homeassistant.components.tts import (
|
||||
CONF_LANG,
|
||||
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
|
||||
Provider,
|
||||
TtsAudioType,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -85,17 +87,17 @@ class BaiduTTSProvider(Provider):
|
||||
}
|
||||
|
||||
@property
|
||||
def default_language(self):
|
||||
def default_language(self) -> str:
|
||||
"""Return the default language."""
|
||||
return self._lang
|
||||
|
||||
@property
|
||||
def supported_languages(self):
|
||||
def supported_languages(self) -> list[str]:
|
||||
"""Return a list of supported languages."""
|
||||
return SUPPORTED_LANGUAGES
|
||||
|
||||
@property
|
||||
def default_options(self):
|
||||
def default_options(self) -> dict[str, Any]:
|
||||
"""Return a dict including default options."""
|
||||
return {
|
||||
CONF_PERSON: self._speech_conf_data[_OPTIONS[CONF_PERSON]],
|
||||
@@ -105,11 +107,16 @@ class BaiduTTSProvider(Provider):
|
||||
}
|
||||
|
||||
@property
|
||||
def supported_options(self):
|
||||
def supported_options(self) -> list[str]:
|
||||
"""Return a list of supported options."""
|
||||
return SUPPORTED_OPTIONS
|
||||
|
||||
def get_tts_audio(self, message, language, options):
|
||||
def get_tts_audio(
|
||||
self,
|
||||
message: str,
|
||||
language: str,
|
||||
options: dict[str, Any],
|
||||
) -> TtsAudioType:
|
||||
"""Load TTS from BaiduTTS."""
|
||||
|
||||
aip_speech = AipSpeech(
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["mozart-api==5.3.1.108.0"],
|
||||
"requirements": ["mozart-api==5.3.1.108.2"],
|
||||
"zeroconf": ["_bangolufsen._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from datetime import timedelta
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from uuid import UUID
|
||||
|
||||
from aiohttp import ClientConnectorError
|
||||
from mozart_api import __version__ as MOZART_API_VERSION
|
||||
@@ -735,7 +736,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
||||
await self._client.set_active_source(source_id=key)
|
||||
else:
|
||||
# Video
|
||||
await self._client.post_remote_trigger(id=key)
|
||||
await self._client.post_remote_trigger(id=UUID(key))
|
||||
|
||||
async def async_select_sound_mode(self, sound_mode: str) -> None:
|
||||
"""Select a sound mode."""
|
||||
@@ -894,7 +895,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
||||
translation_key="play_media_error",
|
||||
translation_placeholders={
|
||||
"media_type": media_type,
|
||||
"error_message": json.loads(error.body)["message"],
|
||||
"error_message": json.loads(cast(str, error.body))["message"],
|
||||
},
|
||||
) from error
|
||||
|
||||
|
||||
@@ -6,16 +6,9 @@ from typing import Any
|
||||
|
||||
from blinkpy.auth import Auth
|
||||
from blinkpy.blinkpy import Blink
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import (
|
||||
CONF_FILE_PATH,
|
||||
CONF_FILENAME,
|
||||
CONF_NAME,
|
||||
CONF_PIN,
|
||||
CONF_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.const import CONF_SCAN_INTERVAL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -27,13 +20,6 @@ from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILENAME): cv.string}
|
||||
)
|
||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema({vol.Optional(CONF_PIN): cv.string})
|
||||
SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILE_PATH): cv.string}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
@@ -9,35 +9,23 @@ from typing import Any
|
||||
from blinkpy.auth import UnauthorizedError
|
||||
from blinkpy.camera import BlinkCamera as BlinkCameraAPI
|
||||
from requests.exceptions import ChunkedEncodingError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
HomeAssistantError,
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
DEFAULT_BRAND,
|
||||
DOMAIN,
|
||||
SERVICE_RECORD,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
SERVICE_TRIGGER,
|
||||
)
|
||||
from .const import DEFAULT_BRAND, DOMAIN
|
||||
from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_VIDEO_CLIP = "video"
|
||||
ATTR_IMAGE = "image"
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@@ -56,20 +44,6 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(SERVICE_RECORD, None, "record")
|
||||
platform.async_register_entity_service(SERVICE_TRIGGER, None, "trigger_camera")
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
{vol.Required(CONF_FILE_PATH): cv.string},
|
||||
"save_recent_clips",
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SAVE_VIDEO,
|
||||
{vol.Required(CONF_FILENAME): cv.string},
|
||||
"save_video",
|
||||
)
|
||||
|
||||
|
||||
class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
|
||||
"""An implementation of a Blink Camera."""
|
||||
|
||||
@@ -20,11 +20,6 @@ TYPE_TEMPERATURE = "temperature"
|
||||
TYPE_BATTERY = "battery"
|
||||
TYPE_WIFI_STRENGTH = "wifi_strength"
|
||||
|
||||
SERVICE_RECORD = "record"
|
||||
SERVICE_TRIGGER = "trigger_camera"
|
||||
SERVICE_SAVE_VIDEO = "save_video"
|
||||
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
|
||||
SERVICE_SEND_PIN = "send_pin"
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
|
||||
@@ -4,13 +4,27 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_PIN
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
CONF_FILE_PATH,
|
||||
CONF_FILENAME,
|
||||
CONF_PIN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir, service
|
||||
|
||||
from .const import DOMAIN, SERVICE_SEND_PIN
|
||||
from .const import DOMAIN
|
||||
|
||||
SERVICE_RECORD = "record"
|
||||
SERVICE_TRIGGER = "trigger_camera"
|
||||
SERVICE_SAVE_VIDEO = "save_video"
|
||||
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
|
||||
|
||||
|
||||
# Deprecated
|
||||
SERVICE_SEND_PIN = "send_pin"
|
||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
@@ -52,3 +66,36 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
_send_pin,
|
||||
schema=SERVICE_SEND_PIN_SCHEMA,
|
||||
)
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_RECORD,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema=None,
|
||||
func="record",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_TRIGGER,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema=None,
|
||||
func="trigger_camera",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_RECENT_CLIPS,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema={vol.Required(CONF_FILE_PATH): cv.string},
|
||||
func="save_recent_clips",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
entity_domain=CAMERA_DOMAIN,
|
||||
schema={vol.Required(CONF_FILENAME): cv.string},
|
||||
func="save_video",
|
||||
)
|
||||
|
||||
@@ -13,14 +13,7 @@ from homeassistant.helpers import config_validation as cv, service
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
ATTR_MASTER,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_TIMER,
|
||||
SERVICE_JOIN,
|
||||
SERVICE_SET_TIMER,
|
||||
SERVICE_UNJOIN,
|
||||
)
|
||||
from .const import ATTR_MASTER, DOMAIN, SERVICE_JOIN, SERVICE_UNJOIN
|
||||
from .coordinator import (
|
||||
BluesoundConfigEntry,
|
||||
BluesoundCoordinator,
|
||||
@@ -37,22 +30,6 @@ PLATFORMS = [
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Bluesound."""
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SET_TIMER,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema=None,
|
||||
func="async_increase_timer",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_TIMER,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema=None,
|
||||
func="async_clear_timer",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
|
||||
@@ -5,7 +5,5 @@ INTEGRATION_TITLE = "Bluesound"
|
||||
ATTR_BLUESOUND_GROUP = "bluesound_group"
|
||||
ATTR_MASTER = "master"
|
||||
|
||||
SERVICE_CLEAR_TIMER = "clear_sleep_timer"
|
||||
SERVICE_JOIN = "join"
|
||||
SERVICE_SET_TIMER = "set_sleep_timer"
|
||||
SERVICE_UNJOIN = "unjoin"
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
{
|
||||
"services": {
|
||||
"clear_sleep_timer": {
|
||||
"service": "mdi:sleep-off"
|
||||
},
|
||||
"join": {
|
||||
"service": "mdi:link-variant"
|
||||
},
|
||||
"set_sleep_timer": {
|
||||
"service": "mdi:sleep"
|
||||
},
|
||||
"unjoin": {
|
||||
"service": "mdi:link-variant-off"
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@ from .const import (
|
||||
ATTR_BLUESOUND_GROUP,
|
||||
ATTR_MASTER,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_TIMER,
|
||||
SERVICE_JOIN,
|
||||
SERVICE_SET_TIMER,
|
||||
SERVICE_UNJOIN,
|
||||
)
|
||||
from .coordinator import BluesoundCoordinator
|
||||
@@ -603,42 +601,6 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
|
||||
"""Remove follower to leader."""
|
||||
await self._player.remove_follower(host, port)
|
||||
|
||||
async def async_increase_timer(self) -> int:
|
||||
"""Increase sleep time on player."""
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_service_{SERVICE_SET_TIMER}",
|
||||
is_fixable=False,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_service_set_sleep_timer",
|
||||
translation_placeholders={
|
||||
"name": slugify(self.sync_status.name),
|
||||
},
|
||||
)
|
||||
return await self._player.sleep_timer()
|
||||
|
||||
async def async_clear_timer(self) -> None:
|
||||
"""Clear sleep timer on player."""
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_service_{SERVICE_CLEAR_TIMER}",
|
||||
is_fixable=False,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_service_clear_sleep_timer",
|
||||
translation_placeholders={
|
||||
"name": slugify(self.sync_status.name),
|
||||
},
|
||||
)
|
||||
sleep = 1
|
||||
while sleep > 0:
|
||||
sleep = await self._player.sleep_timer()
|
||||
|
||||
async def async_set_shuffle(self, shuffle: bool) -> None:
|
||||
"""Enable or disable shuffle mode."""
|
||||
await self._player.shuffle(shuffle)
|
||||
|
||||
@@ -19,19 +19,3 @@ unjoin:
|
||||
entity:
|
||||
integration: bluesound
|
||||
domain: media_player
|
||||
|
||||
set_sleep_timer:
|
||||
fields:
|
||||
entity_id:
|
||||
selector:
|
||||
entity:
|
||||
integration: bluesound
|
||||
domain: media_player
|
||||
|
||||
clear_sleep_timer:
|
||||
fields:
|
||||
entity_id:
|
||||
selector:
|
||||
entity:
|
||||
integration: bluesound
|
||||
domain: media_player
|
||||
|
||||
@@ -37,34 +37,16 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_service_clear_sleep_timer": {
|
||||
"description": "Use `button.{name}_clear_sleep_timer` instead.\n\nPlease replace this action and adjust your automations and scripts.",
|
||||
"title": "Detected use of deprecated action bluesound.clear_sleep_timer"
|
||||
},
|
||||
"deprecated_service_join": {
|
||||
"description": "Use the `media_player.join` action instead.\n\nPlease replace this action and adjust your automations and scripts.",
|
||||
"title": "Detected use of deprecated action bluesound.join"
|
||||
},
|
||||
"deprecated_service_set_sleep_timer": {
|
||||
"description": "Use `button.{name}_set_sleep_timer` instead.\n\nPlease replace this action and adjust your automations and scripts.",
|
||||
"title": "Detected use of deprecated action bluesound.set_sleep_timer"
|
||||
},
|
||||
"deprecated_service_unjoin": {
|
||||
"description": "Use the `media_player.unjoin` action instead.\n\nPlease replace this action and adjust your automations and scripts.",
|
||||
"title": "Detected use of deprecated action bluesound.unjoin"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"clear_sleep_timer": {
|
||||
"description": "Clears a Bluesound timer.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Name(s) of entities that will have the timer cleared.",
|
||||
"name": "Entity"
|
||||
}
|
||||
},
|
||||
"name": "Clear sleep timer"
|
||||
},
|
||||
"join": {
|
||||
"description": "Groups players together under a single master speaker.",
|
||||
"fields": {
|
||||
@@ -79,16 +61,6 @@
|
||||
},
|
||||
"name": "Join"
|
||||
},
|
||||
"set_sleep_timer": {
|
||||
"description": "Sets a Bluesound timer that will turn off the speaker. It will increase in steps: 15, 30, 45, 60, 90, 0.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Name(s) of entities that will have a timer set.",
|
||||
"name": "Entity"
|
||||
}
|
||||
},
|
||||
"name": "Set sleep timer"
|
||||
},
|
||||
"unjoin": {
|
||||
"description": "Separates a player from a group.",
|
||||
"fields": {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==2.0.0",
|
||||
"bleak==2.1.1",
|
||||
"bleak-retry-connector==4.4.3",
|
||||
"bluetooth-adapters==2.1.0",
|
||||
"bluetooth-auto-recovery==1.5.3",
|
||||
|
||||
@@ -16,14 +16,17 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import SLOW_UPDATE_WARNING
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import BRIDGE_MAKE, DOMAIN
|
||||
from .models import BondData
|
||||
from .services import async_setup_services
|
||||
from .utils import BondHub
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [
|
||||
Platform.BUTTON,
|
||||
Platform.COVER,
|
||||
@@ -38,6 +41,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||
type BondConfigEntry = ConfigEntry[BondData]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BondConfigEntry) -> bool:
|
||||
"""Set up Bond from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
|
||||
@@ -5,10 +5,3 @@ BRIDGE_MAKE = "Olibra"
|
||||
DOMAIN = "bond"
|
||||
|
||||
CONF_BOND_ID: str = "bond_id"
|
||||
|
||||
|
||||
SERVICE_SET_FAN_SPEED_TRACKED_STATE = "set_fan_speed_tracked_state"
|
||||
SERVICE_SET_POWER_TRACKED_STATE = "set_switch_power_tracked_state"
|
||||
SERVICE_SET_LIGHT_POWER_TRACKED_STATE = "set_light_power_tracked_state"
|
||||
SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE = "set_light_brightness_tracked_state"
|
||||
ATTR_POWER_STATE = "power_state"
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
from bond_async import Action, DeviceType, Direction
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
DIRECTION_FORWARD,
|
||||
@@ -18,7 +17,6 @@ from homeassistant.components.fan import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.percentage import (
|
||||
percentage_to_ranged_value,
|
||||
@@ -27,7 +25,6 @@ from homeassistant.util.percentage import (
|
||||
from homeassistant.util.scaling import int_states_in_range
|
||||
|
||||
from . import BondConfigEntry
|
||||
from .const import SERVICE_SET_FAN_SPEED_TRACKED_STATE
|
||||
from .entity import BondEntity
|
||||
from .models import BondData
|
||||
from .utils import BondDevice
|
||||
@@ -44,12 +41,6 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Bond fan devices."""
|
||||
data = entry.runtime_data
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_FAN_SPEED_TRACKED_STATE,
|
||||
{vol.Required("speed"): vol.All(vol.Number(scale=0), vol.Range(0, 100))},
|
||||
"async_set_speed_belief",
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
BondFan(data, device)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user