mirror of
https://github.com/home-assistant/core.git
synced 2025-09-22 03:19:33 +00:00
Compare commits
473 Commits
sql-query-
...
2024.9.3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
06ea3a3014 | ||
![]() |
59ecd47374 | ||
![]() |
95053f7114 | ||
![]() |
4949727cd5 | ||
![]() |
08b0064ce7 | ||
![]() |
c9571126a3 | ||
![]() |
06d825d6c8 | ||
![]() |
36e6ab4af8 | ||
![]() |
ccec85f047 | ||
![]() |
e8a5a75e96 | ||
![]() |
4eb1fca68e | ||
![]() |
fba24b8ead | ||
![]() |
edfb9f3f6b | ||
![]() |
2322d071e4 | ||
![]() |
7658ed8eaa | ||
![]() |
c81f280bc1 | ||
![]() |
6e36febd37 | ||
![]() |
b38c193fe4 | ||
![]() |
b336cae118 | ||
![]() |
991114eb7f | ||
![]() |
d924fc5967 | ||
![]() |
c64222de4f | ||
![]() |
b69b5aa82a | ||
![]() |
861fcbe598 | ||
![]() |
d259055af0 | ||
![]() |
fae26ee5da | ||
![]() |
c4eca4469f | ||
![]() |
d91cc96cd2 | ||
![]() |
0b226c1868 | ||
![]() |
359f61e55a | ||
![]() |
16e049b7fa | ||
![]() |
8a6eec925f | ||
![]() |
f365995c8a | ||
![]() |
20ded56c99 | ||
![]() |
4583e070df | ||
![]() |
d4be1f3666 | ||
![]() |
06d4b3281b | ||
![]() |
1dcd5471a0 | ||
![]() |
d0b6ef877e | ||
![]() |
1e63b956f5 | ||
![]() |
7734bdfdab | ||
![]() |
7eb9036cbb | ||
![]() |
6b2526ddbd | ||
![]() |
0b1a898c7c | ||
![]() |
fe247a60ef | ||
![]() |
17402848f2 | ||
![]() |
e6b4c2e700 | ||
![]() |
e7c48d5870 | ||
![]() |
781342be40 | ||
![]() |
73b26407f6 | ||
![]() |
b1d691178e | ||
![]() |
dc189e1d58 | ||
![]() |
444560543c | ||
![]() |
ed2d321746 | ||
![]() |
b50d8fca16 | ||
![]() |
1c7c6d6592 | ||
![]() |
5cf89bf2bb | ||
![]() |
0b95cf1251 | ||
![]() |
a3f42e36ac | ||
![]() |
973e43ae6a | ||
![]() |
e80e189e6b | ||
![]() |
27dc2e1b9d | ||
![]() |
edb7c76caa | ||
![]() |
7859d31ca0 | ||
![]() |
6c640d2abe | ||
![]() |
61ee3a9412 | ||
![]() |
4ed18495f3 | ||
![]() |
5c8b2cde92 | ||
![]() |
5c2073481d | ||
![]() |
84c204a7b3 | ||
![]() |
6c15f251c6 | ||
![]() |
48fcf58eb9 | ||
![]() |
3c13f4b4cc | ||
![]() |
a14826d75e | ||
![]() |
36ec1b33fe | ||
![]() |
84a0a28be2 | ||
![]() |
ac19ee3e2e | ||
![]() |
438af042ed | ||
![]() |
122f11c790 | ||
![]() |
de99dfef4e | ||
![]() |
bcdc3563a5 | ||
![]() |
9ef0a1f0a2 | ||
![]() |
d0629d4e66 | ||
![]() |
65e98eab9c | ||
![]() |
a0d9764443 | ||
![]() |
8293f270df | ||
![]() |
116090bff1 | ||
![]() |
6082220f7f | ||
![]() |
74fd16b953 | ||
![]() |
82cffcbc23 | ||
![]() |
4e1a77326e | ||
![]() |
54cf52069e | ||
![]() |
70b811096c | ||
![]() |
1efd267ee6 | ||
![]() |
31267b4095 | ||
![]() |
4982e1cbcf | ||
![]() |
be3b16b7fa | ||
![]() |
393a0ac0df | ||
![]() |
a0bbcb0401 | ||
![]() |
3f65bc78e8 | ||
![]() |
d005440544 | ||
![]() |
94d2da1685 | ||
![]() |
4c5ba0617a | ||
![]() |
a58bf149fc | ||
![]() |
005be4e8ba | ||
![]() |
b81d7a0ed8 | ||
![]() |
9a690ed421 | ||
![]() |
009989d7ae | ||
![]() |
c7d1ad27f0 | ||
![]() |
3af11fb2b1 | ||
![]() |
c839cc1f15 | ||
![]() |
a0f2e2ebdd | ||
![]() |
d07e62b2f1 | ||
![]() |
e7f957def2 | ||
![]() |
16ab57c9a6 | ||
![]() |
1a67052cbd | ||
![]() |
f85a802ebd | ||
![]() |
3b5c08ecf8 | ||
![]() |
450c63ad28 | ||
![]() |
a8f472f44e | ||
![]() |
fa3a301e97 | ||
![]() |
b1ef1be9a3 | ||
![]() |
62ef951ace | ||
![]() |
06660f9170 | ||
![]() |
7662ca8a96 | ||
![]() |
e04fc74fcf | ||
![]() |
f9bca7619c | ||
![]() |
1b9aa727f8 | ||
![]() |
d54c1935f8 | ||
![]() |
9cfad05793 | ||
![]() |
03ab471d23 | ||
![]() |
b2b69e40fd | ||
![]() |
c6ff445dd4 | ||
![]() |
0948a94409 | ||
![]() |
9be20d6130 | ||
![]() |
c4e484539d | ||
![]() |
234f32265e | ||
![]() |
411b014da2 | ||
![]() |
3a8aa4200d | ||
![]() |
dd8471e786 | ||
![]() |
f33b4b0dc0 | ||
![]() |
8ab8f7a740 | ||
![]() |
ee9e3fe27b | ||
![]() |
d4830caac0 | ||
![]() |
3b4e3b1370 | ||
![]() |
533c8ca31c | ||
![]() |
8668af17f6 | ||
![]() |
5b866e071c | ||
![]() |
37af180edc | ||
![]() |
0d5dc01048 | ||
![]() |
bd2be0a763 | ||
![]() |
98cbd7d8da | ||
![]() |
26f3305743 | ||
![]() |
3c0480596d | ||
![]() |
81d2231e6f | ||
![]() |
2d041a1fa9 | ||
![]() |
03a02fa565 | ||
![]() |
b2f27a4519 | ||
![]() |
b906a1b521 | ||
![]() |
754e4255b6 | ||
![]() |
ff39f09c4e | ||
![]() |
3078b47d06 | ||
![]() |
71de50dae8 | ||
![]() |
e8b722f7b2 | ||
![]() |
a2053d073f | ||
![]() |
aa72b08c16 | ||
![]() |
66480da218 | ||
![]() |
3b214f6610 | ||
![]() |
1e035064f4 | ||
![]() |
2856525c12 | ||
![]() |
22cfb7059a | ||
![]() |
1650cee16c | ||
![]() |
25cdd737a9 | ||
![]() |
9153d16a6d | ||
![]() |
03ead27f6c | ||
![]() |
edad766fd3 | ||
![]() |
9bcc31a9fe | ||
![]() |
7652c35ee6 | ||
![]() |
dd52f4c84a | ||
![]() |
45bb2cdd82 | ||
![]() |
a4bfb0e92c | ||
![]() |
26006f8036 | ||
![]() |
ef4caa951c | ||
![]() |
5824d06fd7 | ||
![]() |
57a73d1b1b | ||
![]() |
731aaaafe2 | ||
![]() |
bdd3aa8e39 | ||
![]() |
99335a07e5 | ||
![]() |
9d633f2087 | ||
![]() |
174f22aa2f | ||
![]() |
e39b3796f3 | ||
![]() |
11370979e5 | ||
![]() |
0afae45bc5 | ||
![]() |
f8ac952cd7 | ||
![]() |
c4e5d67551 | ||
![]() |
cff4e46694 | ||
![]() |
38ef216894 | ||
![]() |
1a2d013c97 | ||
![]() |
cffa8b4feb | ||
![]() |
fce2e21c9f | ||
![]() |
a0ffa69b49 | ||
![]() |
a0089685dd | ||
![]() |
633ff0ea42 | ||
![]() |
163795e73a | ||
![]() |
45eebf3285 | ||
![]() |
14eec2e57a | ||
![]() |
274d98f4d7 | ||
![]() |
51a5a78eb5 | ||
![]() |
d4ae592a85 | ||
![]() |
8ff8ed7f76 | ||
![]() |
18b49a6f62 | ||
![]() |
35d318818a | ||
![]() |
0a94242337 | ||
![]() |
d1681fac72 | ||
![]() |
8504a16e83 | ||
![]() |
41e66edd14 | ||
![]() |
10b3119b4a | ||
![]() |
c772c4a2d5 | ||
![]() |
e9830f0835 | ||
![]() |
bcc66c9a86 | ||
![]() |
1f3c99dff3 | ||
![]() |
b085ac9296 | ||
![]() |
1add00a68d | ||
![]() |
f9bf7f7e05 | ||
![]() |
a63c5e6725 | ||
![]() |
4108b7ada6 | ||
![]() |
42388450e1 | ||
![]() |
e720a14dc4 | ||
![]() |
9d3895d69a | ||
![]() |
16dd6b1712 | ||
![]() |
e447d83024 | ||
![]() |
8fee1975b4 | ||
![]() |
3533ac163c | ||
![]() |
1b304e60d9 | ||
![]() |
5bd17c9198 | ||
![]() |
136f0e423e | ||
![]() |
e84d9e21f7 | ||
![]() |
fa084143ef | ||
![]() |
ca17c70109 | ||
![]() |
5818e2c2d4 | ||
![]() |
467749eb57 | ||
![]() |
dd0c353afb | ||
![]() |
9e762fa222 | ||
![]() |
ea04269c49 | ||
![]() |
e2d84f9a58 | ||
![]() |
52b6f00363 | ||
![]() |
55c42fde88 | ||
![]() |
0dc1eb8757 | ||
![]() |
1936aeccb9 | ||
![]() |
c1c158c0aa | ||
![]() |
bcfc7ea481 | ||
![]() |
de8bbaadd1 | ||
![]() |
318259689f | ||
![]() |
f802611359 | ||
![]() |
53479b5924 | ||
![]() |
9cae786f40 | ||
![]() |
48292beec8 | ||
![]() |
a45ba51f89 | ||
![]() |
9119884e53 | ||
![]() |
37e2839fa3 | ||
![]() |
6b0428774d | ||
![]() |
0d2f22838a | ||
![]() |
4bc19876ca | ||
![]() |
902d76da36 | ||
![]() |
d8161c431f | ||
![]() |
68d6f1c1aa | ||
![]() |
ef1d53c207 | ||
![]() |
823b62d8ab | ||
![]() |
dece7e0f9c | ||
![]() |
68cdf8877c | ||
![]() |
831a1d7ad1 | ||
![]() |
715f4bd2c3 | ||
![]() |
74a12bb802 | ||
![]() |
51fd8e1288 | ||
![]() |
2fe19c04b9 | ||
![]() |
37019d33fd | ||
![]() |
0fe939cd7c | ||
![]() |
b960ebeb8b | ||
![]() |
7334fb0125 | ||
![]() |
d8fe3c5377 | ||
![]() |
76182c246d | ||
![]() |
b9aaba0432 | ||
![]() |
156948c496 | ||
![]() |
547dbf77aa | ||
![]() |
a68cd712c6 | ||
![]() |
9e7aeed848 | ||
![]() |
95fa123a0b | ||
![]() |
657ff58500 | ||
![]() |
ff029e5efc | ||
![]() |
f4528b288f | ||
![]() |
1aa0dbdaf5 | ||
![]() |
743df84569 | ||
![]() |
16231da5ef | ||
![]() |
7b71f024fb | ||
![]() |
0a05cdc381 | ||
![]() |
106559371c | ||
![]() |
cafd953f87 | ||
![]() |
7ddd755acc | ||
![]() |
302ffe5e56 | ||
![]() |
65216df3a5 | ||
![]() |
76ebb0df08 | ||
![]() |
0591b5e47b | ||
![]() |
bb6f9ec844 | ||
![]() |
592f60643a | ||
![]() |
3035588dfa | ||
![]() |
6e727a49bf | ||
![]() |
0dfe12840d | ||
![]() |
d915fee833 | ||
![]() |
9ea41b4d26 | ||
![]() |
8bc9fd23bb | ||
![]() |
18212052a4 | ||
![]() |
3304e27fa3 | ||
![]() |
be206156b0 | ||
![]() |
0628f96713 | ||
![]() |
41b129e990 | ||
![]() |
ebc49d938a | ||
![]() |
58b7711bdd | ||
![]() |
909dfcc436 | ||
![]() |
70fc8fa2eb | ||
![]() |
242aae514e | ||
![]() |
d94b1e6e8a | ||
![]() |
d18e81f932 | ||
![]() |
d3293336b1 | ||
![]() |
1eeb3bdcdf | ||
![]() |
36bfd7b9ce | ||
![]() |
f84a04e113 | ||
![]() |
516f3295bf | ||
![]() |
2d5289e7dd | ||
![]() |
18efd84a35 | ||
![]() |
b34c90b189 | ||
![]() |
a45c1a3914 | ||
![]() |
1bdf9d657e | ||
![]() |
b294a92ad2 | ||
![]() |
2db362ab3d | ||
![]() |
5f275a6b9c | ||
![]() |
fa914b2811 | ||
![]() |
a128e2e4fc | ||
![]() |
03c7f2cf5b | ||
![]() |
102528e5d3 | ||
![]() |
8f4af4f7c2 | ||
![]() |
667af10017 | ||
![]() |
e5a64a1e0a | ||
![]() |
236fa8e238 | ||
![]() |
70a58a0bb0 | ||
![]() |
769c7f1ea3 | ||
![]() |
5a8045d1fb | ||
![]() |
5a73b636e3 | ||
![]() |
524e09b45e | ||
![]() |
1f46670266 | ||
![]() |
a857f603c8 | ||
![]() |
b7d8f3d005 | ||
![]() |
129035967b | ||
![]() |
45b44f8a59 | ||
![]() |
e80dc52175 | ||
![]() |
22bb3e5477 | ||
![]() |
f89e8e6ceb | ||
![]() |
157a61845b | ||
![]() |
0fcdc3c200 | ||
![]() |
d1f09ecd0c | ||
![]() |
3484ab3c0c | ||
![]() |
80df582ebd | ||
![]() |
dc967e2ef2 | ||
![]() |
e2c1a38d87 | ||
![]() |
c9e7c76ee5 | ||
![]() |
5550b1a74e | ||
![]() |
51b520db0c | ||
![]() |
f06c21c8cc | ||
![]() |
fcf0bef2cd | ||
![]() |
7c8e00e5e0 | ||
![]() |
ce72157bf7 | ||
![]() |
31f5539311 | ||
![]() |
b63fb9f17f | ||
![]() |
9b3718edfb | ||
![]() |
156e39ebb2 | ||
![]() |
c506188c13 | ||
![]() |
b64c21cce6 | ||
![]() |
e85755fbda | ||
![]() |
3e3d27f48d | ||
![]() |
32f75597a9 | ||
![]() |
d7d35f74f2 | ||
![]() |
5dc03752ca | ||
![]() |
b26446bd88 | ||
![]() |
9e13184256 | ||
![]() |
b7170c78a5 | ||
![]() |
e26d363b5e | ||
![]() |
7ae8f4c9d0 | ||
![]() |
664e0258bf | ||
![]() |
8023cbcc38 | ||
![]() |
22c322fc37 | ||
![]() |
2bb4a8747c | ||
![]() |
098a006f32 | ||
![]() |
d47a296d7a | ||
![]() |
c2ce71a38c | ||
![]() |
a7c6abc54e | ||
![]() |
79ba315008 | ||
![]() |
26e87509be | ||
![]() |
af2fec89d8 | ||
![]() |
61cee043e6 | ||
![]() |
fd57931cc9 | ||
![]() |
44b6bca89a | ||
![]() |
0bd9386df2 | ||
![]() |
94516de724 | ||
![]() |
a2027fc78c | ||
![]() |
be5577c2f9 | ||
![]() |
93dc08a05f | ||
![]() |
def2ace4ec | ||
![]() |
4f0261d739 | ||
![]() |
6103811de8 | ||
![]() |
fd904c65a7 | ||
![]() |
04bf8482b2 | ||
![]() |
f5fd5e0457 | ||
![]() |
0de89b42aa | ||
![]() |
e8914552b1 | ||
![]() |
bfd302109e | ||
![]() |
796ad47dd0 | ||
![]() |
e9915463a9 | ||
![]() |
59aecda8cf | ||
![]() |
7d00ccbbbc | ||
![]() |
55a911120c | ||
![]() |
80abf90c87 | ||
![]() |
8539591307 | ||
![]() |
6234deeee1 | ||
![]() |
81fabb1bfa | ||
![]() |
ff4e5859cf | ||
![]() |
f2e42eafc7 | ||
![]() |
63f28ae2fe | ||
![]() |
5b6c6141c5 | ||
![]() |
396ef7a642 | ||
![]() |
17f59a5665 | ||
![]() |
10846dc97b | ||
![]() |
17bb00727d | ||
![]() |
bc021dbbc6 | ||
![]() |
e3cb9c0844 | ||
![]() |
050e2c9404 | ||
![]() |
5ea447ba48 | ||
![]() |
a23b063922 | ||
![]() |
c269d57259 | ||
![]() |
d512f327c5 | ||
![]() |
9bf8c5a54b | ||
![]() |
725e2f16f5 | ||
![]() |
d98d0cdad0 | ||
![]() |
e2f4aa893f | ||
![]() |
6b81fa89d3 | ||
![]() |
c886587915 | ||
![]() |
059d3eed98 | ||
![]() |
f9ae2b4453 | ||
![]() |
742c7ba23f | ||
![]() |
e7ae5c5c24 | ||
![]() |
ae4fc9504a | ||
![]() |
2ef337ec2e | ||
![]() |
723b7bd532 | ||
![]() |
4fdb11b0d8 | ||
![]() |
fe2e6c37f4 | ||
![]() |
4a75c55a8f | ||
![]() |
dfb59469cf | ||
![]() |
bdb2e1e2e9 | ||
![]() |
c4f6f1e3d8 | ||
![]() |
fb3eae54ea | ||
![]() |
d3f8fce788 | ||
![]() |
44e58a8c87 | ||
![]() |
3d3879b0db | ||
![]() |
a8b1eb34f3 | ||
![]() |
fd77058def | ||
![]() |
b147ca6c5b | ||
![]() |
670c4cacfa | ||
![]() |
1ed0a89303 | ||
![]() |
ab0597da7b | ||
![]() |
a3db6bc8fa | ||
![]() |
9bfc8f6e27 | ||
![]() |
6fddef2dc5 | ||
![]() |
ec08a85aa0 | ||
![]() |
de7af575c5 | ||
![]() |
d3831bae4e |
@@ -61,6 +61,7 @@ components: &components
|
|||||||
- homeassistant/components/auth/**
|
- homeassistant/components/auth/**
|
||||||
- homeassistant/components/automation/**
|
- homeassistant/components/automation/**
|
||||||
- homeassistant/components/backup/**
|
- homeassistant/components/backup/**
|
||||||
|
- homeassistant/components/blueprint/**
|
||||||
- homeassistant/components/bluetooth/**
|
- homeassistant/components/bluetooth/**
|
||||||
- homeassistant/components/cloud/**
|
- homeassistant/components/cloud/**
|
||||||
- homeassistant/components/config/**
|
- homeassistant/components/config/**
|
||||||
|
53
.github/workflows/builder.yml
vendored
53
.github/workflows/builder.yml
vendored
@@ -482,3 +482,56 @@ jobs:
|
|||||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||||
|
|
||||||
twine upload dist/* --skip-existing
|
twine upload dist/* --skip-existing
|
||||||
|
|
||||||
|
hassfest-image:
|
||||||
|
name: Build and test hassfest image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
attestations: write
|
||||||
|
id-token: write
|
||||||
|
needs: ["init"]
|
||||||
|
if: github.repository_owner == 'home-assistant'
|
||||||
|
env:
|
||||||
|
HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
|
||||||
|
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
||||||
|
with:
|
||||||
|
context: . # So action will not pull the repository again
|
||||||
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
|
load: true
|
||||||
|
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
||||||
|
|
||||||
|
- name: Run hassfest against core
|
||||||
|
run: docker run --rm -v ${{ github.workspace }}/homeassistant:/github/workspace/homeassistant ${{ env.HASSFEST_IMAGE_TAG }} --core-integrations-path=/github/workspace/homeassistant/components
|
||||||
|
|
||||||
|
- name: Push Docker image
|
||||||
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
|
id: push
|
||||||
|
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
||||||
|
with:
|
||||||
|
context: . # So action will not pull the repository again
|
||||||
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
|
||||||
|
|
||||||
|
- name: Generate artifact attestation
|
||||||
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
|
uses: actions/attest-build-provenance@6149ea5740be74af77f260b9db67e633f6b0a9a1 # v1.4.2
|
||||||
|
with:
|
||||||
|
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||||
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
push-to-registry: true
|
||||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
|||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.26.4
|
uses: github/codeql-action/init@v3.26.5
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.26.4
|
uses: github/codeql-action/analyze@v3.26.5
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
@@ -294,7 +294,6 @@ homeassistant.components.london_underground.*
|
|||||||
homeassistant.components.lookin.*
|
homeassistant.components.lookin.*
|
||||||
homeassistant.components.luftdaten.*
|
homeassistant.components.luftdaten.*
|
||||||
homeassistant.components.madvr.*
|
homeassistant.components.madvr.*
|
||||||
homeassistant.components.mailbox.*
|
|
||||||
homeassistant.components.manual.*
|
homeassistant.components.manual.*
|
||||||
homeassistant.components.map.*
|
homeassistant.components.map.*
|
||||||
homeassistant.components.mastodon.*
|
homeassistant.components.mastodon.*
|
||||||
|
@@ -1493,6 +1493,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tomorrowio/ @raman325 @lymanepp
|
/tests/components/tomorrowio/ @raman325 @lymanepp
|
||||||
/homeassistant/components/totalconnect/ @austinmroczek
|
/homeassistant/components/totalconnect/ @austinmroczek
|
||||||
/tests/components/totalconnect/ @austinmroczek
|
/tests/components/totalconnect/ @austinmroczek
|
||||||
|
/homeassistant/components/touchline_sl/ @jnsgruk
|
||||||
|
/tests/components/touchline_sl/ @jnsgruk
|
||||||
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
||||||
/tests/components/tplink/ @rytilahti @bdraco @sdb9696
|
/tests/components/tplink/ @rytilahti @bdraco @sdb9696
|
||||||
/homeassistant/components/tplink_omada/ @MarkGodwin
|
/homeassistant/components/tplink_omada/ @MarkGodwin
|
||||||
@@ -1658,6 +1660,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG
|
/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG
|
||||||
/homeassistant/components/xiaomi_tv/ @simse
|
/homeassistant/components/xiaomi_tv/ @simse
|
||||||
/homeassistant/components/xmpp/ @fabaff @flowolf
|
/homeassistant/components/xmpp/ @fabaff @flowolf
|
||||||
|
/homeassistant/components/yale/ @bdraco
|
||||||
|
/tests/components/yale/ @bdraco
|
||||||
/homeassistant/components/yale_smart_alarm/ @gjohansson-ST
|
/homeassistant/components/yale_smart_alarm/ @gjohansson-ST
|
||||||
/tests/components/yale_smart_alarm/ @gjohansson-ST
|
/tests/components/yale_smart_alarm/ @gjohansson-ST
|
||||||
/homeassistant/components/yalexs_ble/ @bdraco
|
/homeassistant/components/yalexs_ble/ @bdraco
|
||||||
|
@@ -42,7 +42,8 @@ WORKDIR /usr/src
|
|||||||
|
|
||||||
# Setup hass-release
|
# Setup hass-release
|
||||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
||||||
&& uv pip install --system -e hass-release/
|
&& uv pip install --system -e hass-release/ \
|
||||||
|
&& chown -R vscode /usr/src/hass-release/data
|
||||||
|
|
||||||
USER vscode
|
USER vscode
|
||||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||||
|
5
homeassistant/brands/roth.json
Normal file
5
homeassistant/brands/roth.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "roth",
|
||||||
|
"name": "Roth",
|
||||||
|
"integrations": ["touchline", "touchline_sl"]
|
||||||
|
}
|
@@ -1,5 +1,11 @@
|
|||||||
{
|
{
|
||||||
"domain": "yale",
|
"domain": "yale",
|
||||||
"name": "Yale",
|
"name": "Yale",
|
||||||
"integrations": ["august", "yale_smart_alarm", "yalexs_ble", "yale_home"]
|
"integrations": [
|
||||||
|
"august",
|
||||||
|
"yale_smart_alarm",
|
||||||
|
"yalexs_ble",
|
||||||
|
"yale_home",
|
||||||
|
"yale"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@@ -7,8 +7,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"capture_image": "mdi:camera",
|
"capture_image": {
|
||||||
"change_setting": "mdi:cog",
|
"service": "mdi:camera"
|
||||||
"trigger_automation": "mdi:play"
|
},
|
||||||
|
"change_setting": {
|
||||||
|
"service": "mdi:cog"
|
||||||
|
},
|
||||||
|
"trigger_automation": {
|
||||||
|
"service": "mdi:play"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/acmeda",
|
"documentation": "https://www.home-assistant.io/integrations/acmeda",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aiopulse"],
|
"loggers": ["aiopulse"],
|
||||||
"requirements": ["aiopulse==0.4.4"]
|
"requirements": ["aiopulse==0.4.6"]
|
||||||
}
|
}
|
||||||
|
@@ -66,10 +66,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"add_url": "mdi:link-plus",
|
"add_url": {
|
||||||
"remove_url": "mdi:link-off",
|
"service": "mdi:link-plus"
|
||||||
"enable_url": "mdi:link-variant",
|
},
|
||||||
"disable_url": "mdi:link-variant-off",
|
"remove_url": {
|
||||||
"refresh": "mdi:refresh"
|
"service": "mdi:link-off"
|
||||||
|
},
|
||||||
|
"enable_url": {
|
||||||
|
"service": "mdi:link-variant"
|
||||||
|
},
|
||||||
|
"disable_url": {
|
||||||
|
"service": "mdi:link-variant-off"
|
||||||
|
},
|
||||||
|
"refresh": {
|
||||||
|
"service": "mdi:refresh"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"write_data_by_name": "mdi:pencil"
|
"write_data_by_name": {
|
||||||
|
"service": "mdi:pencil"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"set_time_to": "mdi:timer-cog"
|
"set_time_to": {
|
||||||
|
"service": "mdi:timer-cog"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"add_tracking": "mdi:package-variant-plus",
|
"add_tracking": {
|
||||||
"remove_tracking": "mdi:package-variant-minus"
|
"service": "mdi:package-variant-plus"
|
||||||
|
},
|
||||||
|
"remove_tracking": {
|
||||||
|
"service": "mdi:package-variant-minus"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,19 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"start_recording": "mdi:record-rec",
|
"start_recording": {
|
||||||
"stop_recording": "mdi:stop",
|
"service": "mdi:record-rec"
|
||||||
"enable_alerts": "mdi:bell-alert",
|
},
|
||||||
"disable_alerts": "mdi:bell-off",
|
"stop_recording": {
|
||||||
"snapshot": "mdi:camera"
|
"service": "mdi:stop"
|
||||||
|
},
|
||||||
|
"enable_alerts": {
|
||||||
|
"service": "mdi:bell-alert"
|
||||||
|
},
|
||||||
|
"disable_alerts": {
|
||||||
|
"service": "mdi:bell-off"
|
||||||
|
},
|
||||||
|
"snapshot": {
|
||||||
|
"service": "mdi:camera"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -92,7 +92,9 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
except AirGradientError:
|
except AirGradientError:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(current_measures.serial_number)
|
await self.async_set_unique_id(
|
||||||
|
current_measures.serial_number, raise_on_progress=False
|
||||||
|
)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
await self.set_configuration_source()
|
await self.set_configuration_source()
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
|
@@ -6,6 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["airgradient==0.8.0"],
|
"requirements": ["airgradient==0.9.0"],
|
||||||
"zeroconf": ["_airgradient._tcp.local."]
|
"zeroconf": ["_airgradient._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -24,5 +24,5 @@
|
|||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["airthings-ble==0.9.0"]
|
"requirements": ["airthings-ble==0.9.1"]
|
||||||
}
|
}
|
||||||
|
@@ -262,7 +262,7 @@ class Airtouch5AC(Airtouch5ClimateEntity):
|
|||||||
_LOGGER.debug("Argument `temperature` is missing in set_temperature")
|
_LOGGER.debug("Argument `temperature` is missing in set_temperature")
|
||||||
return
|
return
|
||||||
|
|
||||||
await self._control(temp=temp)
|
await self._control(setpoint=SetpointControl.CHANGE_SETPOINT, temp=temp)
|
||||||
|
|
||||||
|
|
||||||
class Airtouch5Zone(Airtouch5ClimateEntity):
|
class Airtouch5Zone(Airtouch5ClimateEntity):
|
||||||
|
@@ -80,11 +80,9 @@ class AirVisualProFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self._reauth_entry: ConfigEntry | None = None
|
self._reauth_entry: ConfigEntry | None = None
|
||||||
|
|
||||||
async def async_step_import(
|
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||||
self, import_config: dict[str, Any]
|
"""Import a config entry from `airvisual` integration (see #83882)."""
|
||||||
) -> ConfigFlowResult:
|
return await self.async_step_user(import_data)
|
||||||
"""Import a config entry from configuration.yaml."""
|
|
||||||
return await self.async_step_user(import_config)
|
|
||||||
|
|
||||||
async def async_step_reauth(
|
async def async_step_reauth(
|
||||||
self, entry_data: Mapping[str, Any]
|
self, entry_data: Mapping[str, Any]
|
||||||
|
@@ -2,16 +2,21 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
from aioairzone.common import GrilleAngle, SleepTimeout
|
from aioairzone.common import GrilleAngle, OperationMode, SleepTimeout
|
||||||
from aioairzone.const import (
|
from aioairzone.const import (
|
||||||
API_COLD_ANGLE,
|
API_COLD_ANGLE,
|
||||||
API_HEAT_ANGLE,
|
API_HEAT_ANGLE,
|
||||||
|
API_MODE,
|
||||||
API_SLEEP,
|
API_SLEEP,
|
||||||
AZD_COLD_ANGLE,
|
AZD_COLD_ANGLE,
|
||||||
AZD_HEAT_ANGLE,
|
AZD_HEAT_ANGLE,
|
||||||
|
AZD_MASTER,
|
||||||
|
AZD_MODE,
|
||||||
|
AZD_MODES,
|
||||||
AZD_SLEEP,
|
AZD_SLEEP,
|
||||||
AZD_ZONES,
|
AZD_ZONES,
|
||||||
)
|
)
|
||||||
@@ -33,6 +38,9 @@ class AirzoneSelectDescription(SelectEntityDescription):
|
|||||||
|
|
||||||
api_param: str
|
api_param: str
|
||||||
options_dict: dict[str, int]
|
options_dict: dict[str, int]
|
||||||
|
options_fn: Callable[[dict[str, Any], dict[str, int]], list[str]] = (
|
||||||
|
lambda zone_data, value: list(value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
GRILLE_ANGLE_DICT: Final[dict[str, int]] = {
|
GRILLE_ANGLE_DICT: Final[dict[str, int]] = {
|
||||||
@@ -42,6 +50,15 @@ GRILLE_ANGLE_DICT: Final[dict[str, int]] = {
|
|||||||
"40deg": GrilleAngle.DEG_40,
|
"40deg": GrilleAngle.DEG_40,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MODE_DICT: Final[dict[str, int]] = {
|
||||||
|
"cool": OperationMode.COOLING,
|
||||||
|
"dry": OperationMode.DRY,
|
||||||
|
"fan": OperationMode.FAN,
|
||||||
|
"heat": OperationMode.HEATING,
|
||||||
|
"heat_cool": OperationMode.AUTO,
|
||||||
|
"stop": OperationMode.STOP,
|
||||||
|
}
|
||||||
|
|
||||||
SLEEP_DICT: Final[dict[str, int]] = {
|
SLEEP_DICT: Final[dict[str, int]] = {
|
||||||
"off": SleepTimeout.SLEEP_OFF,
|
"off": SleepTimeout.SLEEP_OFF,
|
||||||
"30m": SleepTimeout.SLEEP_30,
|
"30m": SleepTimeout.SLEEP_30,
|
||||||
@@ -50,6 +67,26 @@ SLEEP_DICT: Final[dict[str, int]] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main_zone_options(
|
||||||
|
zone_data: dict[str, Any],
|
||||||
|
options: dict[str, int],
|
||||||
|
) -> list[str]:
|
||||||
|
"""Filter available modes."""
|
||||||
|
modes = zone_data.get(AZD_MODES, [])
|
||||||
|
return [k for k, v in options.items() if v in modes]
|
||||||
|
|
||||||
|
|
||||||
|
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||||
|
AirzoneSelectDescription(
|
||||||
|
api_param=API_MODE,
|
||||||
|
key=AZD_MODE,
|
||||||
|
options_dict=MODE_DICT,
|
||||||
|
options_fn=main_zone_options,
|
||||||
|
translation_key="modes",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||||
AirzoneSelectDescription(
|
AirzoneSelectDescription(
|
||||||
api_param=API_COLD_ANGLE,
|
api_param=API_COLD_ANGLE,
|
||||||
@@ -95,7 +132,20 @@ async def async_setup_entry(
|
|||||||
received_zones = set(zones_data)
|
received_zones = set(zones_data)
|
||||||
new_zones = received_zones - added_zones
|
new_zones = received_zones - added_zones
|
||||||
if new_zones:
|
if new_zones:
|
||||||
async_add_entities(
|
entities: list[AirzoneZoneSelect] = [
|
||||||
|
AirzoneZoneSelect(
|
||||||
|
coordinator,
|
||||||
|
description,
|
||||||
|
entry,
|
||||||
|
system_zone_id,
|
||||||
|
zones_data.get(system_zone_id),
|
||||||
|
)
|
||||||
|
for system_zone_id in new_zones
|
||||||
|
for description in MAIN_ZONE_SELECT_TYPES
|
||||||
|
if description.key in zones_data.get(system_zone_id)
|
||||||
|
and zones_data.get(system_zone_id).get(AZD_MASTER) is True
|
||||||
|
]
|
||||||
|
entities += [
|
||||||
AirzoneZoneSelect(
|
AirzoneZoneSelect(
|
||||||
coordinator,
|
coordinator,
|
||||||
description,
|
description,
|
||||||
@@ -106,7 +156,8 @@ async def async_setup_entry(
|
|||||||
for system_zone_id in new_zones
|
for system_zone_id in new_zones
|
||||||
for description in ZONE_SELECT_TYPES
|
for description in ZONE_SELECT_TYPES
|
||||||
if description.key in zones_data.get(system_zone_id)
|
if description.key in zones_data.get(system_zone_id)
|
||||||
)
|
]
|
||||||
|
async_add_entities(entities)
|
||||||
added_zones.update(new_zones)
|
added_zones.update(new_zones)
|
||||||
|
|
||||||
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||||
@@ -153,6 +204,11 @@ class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
|
|||||||
f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
|
f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
|
||||||
)
|
)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
|
self._attr_options = self.entity_description.options_fn(
|
||||||
|
zone_data, description.options_dict
|
||||||
|
)
|
||||||
|
|
||||||
self.values_dict = {v: k for k, v in description.options_dict.items()}
|
self.values_dict = {v: k for k, v in description.options_dict.items()}
|
||||||
|
|
||||||
self._async_update_attrs()
|
self._async_update_attrs()
|
||||||
|
@@ -52,6 +52,17 @@
|
|||||||
"40deg": "[%key:component::airzone::entity::select::grille_angles::state::40deg%]"
|
"40deg": "[%key:component::airzone::entity::select::grille_angles::state::40deg%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"modes": {
|
||||||
|
"name": "Mode",
|
||||||
|
"state": {
|
||||||
|
"cool": "[%key:component::climate::entity_component::_::state::cool%]",
|
||||||
|
"dry": "[%key:component::climate::entity_component::_::state::dry%]",
|
||||||
|
"fan": "[%key:component::climate::entity_component::_::state::fan_only%]",
|
||||||
|
"heat": "[%key:component::climate::entity_component::_::state::heat%]",
|
||||||
|
"heat_cool": "[%key:component::climate::entity_component::_::state::heat_cool%]",
|
||||||
|
"stop": "Stop"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sleep_times": {
|
"sleep_times": {
|
||||||
"name": "Sleep",
|
"name": "Sleep",
|
||||||
"state": {
|
"state": {
|
||||||
|
@@ -15,12 +15,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"alarm_arm_away": "mdi:shield-lock",
|
"alarm_arm_away": {
|
||||||
"alarm_arm_home": "mdi:shield-home",
|
"service": "mdi:shield-lock"
|
||||||
"alarm_arm_night": "mdi:shield-moon",
|
},
|
||||||
"alarm_arm_custom_bypass": "mdi:security",
|
"alarm_arm_home": {
|
||||||
"alarm_disarm": "mdi:shield-off",
|
"service": "mdi:shield-home"
|
||||||
"alarm_trigger": "mdi:bell-ring",
|
},
|
||||||
"alarm_arm_vacation": "mdi:shield-airplane"
|
"alarm_arm_night": {
|
||||||
|
"service": "mdi:shield-moon"
|
||||||
|
},
|
||||||
|
"alarm_arm_custom_bypass": {
|
||||||
|
"service": "mdi:security"
|
||||||
|
},
|
||||||
|
"alarm_disarm": {
|
||||||
|
"service": "mdi:shield-off"
|
||||||
|
},
|
||||||
|
"alarm_trigger": {
|
||||||
|
"service": "mdi:bell-ring"
|
||||||
|
},
|
||||||
|
"alarm_arm_vacation": {
|
||||||
|
"service": "mdi:shield-airplane"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"alarm_keypress": "mdi:dialpad",
|
"alarm_keypress": {
|
||||||
"alarm_toggle_chime": "mdi:abc"
|
"service": "mdi:dialpad"
|
||||||
|
},
|
||||||
|
"alarm_toggle_chime": {
|
||||||
|
"service": "mdi:abc"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"toggle": "mdi:bell-ring",
|
"toggle": {
|
||||||
"turn_off": "mdi:bell-off",
|
"service": "mdi:bell-ring"
|
||||||
"turn_on": "mdi:bell-alert"
|
},
|
||||||
|
"turn_off": {
|
||||||
|
"service": "mdi:bell-off"
|
||||||
|
},
|
||||||
|
"turn_on": {
|
||||||
|
"service": "mdi:bell-alert"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -661,9 +661,12 @@ class RemoteCapabilities(AlexaEntity):
|
|||||||
def interfaces(self) -> Generator[AlexaCapability]:
|
def interfaces(self) -> Generator[AlexaCapability]:
|
||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
yield AlexaPowerController(self.entity)
|
yield AlexaPowerController(self.entity)
|
||||||
yield AlexaModeController(
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
|
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
|
||||||
)
|
if activities and supported & remote.RemoteEntityFeature.ACTIVITY:
|
||||||
|
yield AlexaModeController(
|
||||||
|
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
|
||||||
|
)
|
||||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
yield Alexa(self.entity)
|
yield Alexa(self.entity)
|
||||||
|
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioambient"],
|
"loggers": ["aioambient"],
|
||||||
"requirements": ["aioambient==2024.01.0"]
|
"requirements": ["aioambient==2024.08.0"]
|
||||||
}
|
}
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioambient"],
|
"loggers": ["aioambient"],
|
||||||
"requirements": ["aioambient==2024.01.0"]
|
"requirements": ["aioambient==2024.08.0"]
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,37 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"enable_recording": "mdi:record-rec",
|
"enable_recording": {
|
||||||
"disable_recording": "mdi:stop",
|
"service": "mdi:record-rec"
|
||||||
"enable_audio": "mdi:volume-high",
|
},
|
||||||
"disable_audio": "mdi:volume-off",
|
"disable_recording": {
|
||||||
"enable_motion_recording": "mdi:motion-sensor",
|
"service": "mdi:stop"
|
||||||
"disable_motion_recording": "mdi:motion-sensor-off",
|
},
|
||||||
"goto_preset": "mdi:pan",
|
"enable_audio": {
|
||||||
"set_color_bw": "mdi:palette",
|
"service": "mdi:volume-high"
|
||||||
"start_tour": "mdi:panorama",
|
},
|
||||||
"stop_tour": "mdi:panorama-outline",
|
"disable_audio": {
|
||||||
"ptz_control": "mdi:pan"
|
"service": "mdi:volume-off"
|
||||||
|
},
|
||||||
|
"enable_motion_recording": {
|
||||||
|
"service": "mdi:motion-sensor"
|
||||||
|
},
|
||||||
|
"disable_motion_recording": {
|
||||||
|
"service": "mdi:motion-sensor-off"
|
||||||
|
},
|
||||||
|
"goto_preset": {
|
||||||
|
"service": "mdi:pan"
|
||||||
|
},
|
||||||
|
"set_color_bw": {
|
||||||
|
"service": "mdi:palette"
|
||||||
|
},
|
||||||
|
"start_tour": {
|
||||||
|
"service": "mdi:panorama"
|
||||||
|
},
|
||||||
|
"stop_tour": {
|
||||||
|
"service": "mdi:panorama-outline"
|
||||||
|
},
|
||||||
|
"ptz_control": {
|
||||||
|
"service": "mdi:pan"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,16 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"adb_command": "mdi:console",
|
"adb_command": {
|
||||||
"download": "mdi:download",
|
"service": "mdi:console"
|
||||||
"upload": "mdi:upload",
|
},
|
||||||
"learn_sendevent": "mdi:remote"
|
"download": {
|
||||||
|
"service": "mdi:download"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"service": "mdi:upload"
|
||||||
|
},
|
||||||
|
"learn_sendevent": {
|
||||||
|
"service": "mdi:remote"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,6 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["androidtvremote2"],
|
"loggers": ["androidtvremote2"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["androidtvremote2==0.1.1"],
|
"requirements": ["androidtvremote2==0.1.2"],
|
||||||
"zeroconf": ["_androidtvremote2._tcp.local."]
|
"zeroconf": ["_androidtvremote2._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ LOGGER = logging.getLogger(__package__)
|
|||||||
CONF_RECOMMENDED = "recommended"
|
CONF_RECOMMENDED = "recommended"
|
||||||
CONF_PROMPT = "prompt"
|
CONF_PROMPT = "prompt"
|
||||||
CONF_CHAT_MODEL = "chat_model"
|
CONF_CHAT_MODEL = "chat_model"
|
||||||
RECOMMENDED_CHAT_MODEL = "claude-3-5-sonnet-20240620"
|
RECOMMENDED_CHAT_MODEL = "claude-3-haiku-20240307"
|
||||||
CONF_MAX_TOKENS = "max_tokens"
|
CONF_MAX_TOKENS = "max_tokens"
|
||||||
RECOMMENDED_MAX_TOKENS = 1024
|
RECOMMENDED_MAX_TOKENS = 1024
|
||||||
CONF_TEMPERATURE = "temperature"
|
CONF_TEMPERATURE = "temperature"
|
||||||
|
@@ -6,4 +6,4 @@ DOMAIN: Final = "apcupsd"
|
|||||||
CONNECTION_TIMEOUT: int = 10
|
CONNECTION_TIMEOUT: int = 10
|
||||||
|
|
||||||
# Field name of last self test retrieved from apcupsd.
|
# Field name of last self test retrieved from apcupsd.
|
||||||
LASTSTEST: Final = "laststest"
|
LAST_S_TEST: Final = "laststest"
|
||||||
|
@@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
STATE_UNKNOWN,
|
|
||||||
UnitOfApparentPower,
|
UnitOfApparentPower,
|
||||||
UnitOfElectricCurrent,
|
UnitOfElectricCurrent,
|
||||||
UnitOfElectricPotential,
|
UnitOfElectricPotential,
|
||||||
@@ -26,7 +25,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN, LASTSTEST
|
from .const import DOMAIN, LAST_S_TEST
|
||||||
from .coordinator import APCUPSdCoordinator
|
from .coordinator import APCUPSdCoordinator
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@@ -157,8 +156,8 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
|||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
LASTSTEST: SensorEntityDescription(
|
LAST_S_TEST: SensorEntityDescription(
|
||||||
key=LASTSTEST,
|
key=LAST_S_TEST,
|
||||||
translation_key="last_self_test",
|
translation_key="last_self_test",
|
||||||
),
|
),
|
||||||
"lastxfer": SensorEntityDescription(
|
"lastxfer": SensorEntityDescription(
|
||||||
@@ -423,7 +422,7 @@ async def async_setup_entry(
|
|||||||
# periodical (or manual) self test since last daemon restart. It might not be available
|
# periodical (or manual) self test since last daemon restart. It might not be available
|
||||||
# when we set up the integration, and we do not know if it would ever be available. Here we
|
# when we set up the integration, and we do not know if it would ever be available. Here we
|
||||||
# add it anyway and mark it as unknown initially.
|
# add it anyway and mark it as unknown initially.
|
||||||
for resource in available_resources | {LASTSTEST}:
|
for resource in available_resources | {LAST_S_TEST}:
|
||||||
if resource not in SENSORS:
|
if resource not in SENSORS:
|
||||||
_LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper())
|
_LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper())
|
||||||
continue
|
continue
|
||||||
@@ -484,7 +483,7 @@ class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
|
|||||||
# performed) and may disappear again after certain event. So we mark the state as "unknown"
|
# performed) and may disappear again after certain event. So we mark the state as "unknown"
|
||||||
# when it becomes unknown after such events.
|
# when it becomes unknown after such events.
|
||||||
if key not in self.coordinator.data:
|
if key not in self.coordinator.data:
|
||||||
self._attr_native_value = STATE_UNKNOWN
|
self._attr_native_value = None
|
||||||
return
|
return
|
||||||
|
|
||||||
self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key])
|
self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key])
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyatv", "srptools"],
|
"loggers": ["pyatv", "srptools"],
|
||||||
"requirements": ["pyatv==0.15.0"],
|
"requirements": ["pyatv==0.15.1"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
"_mediaremotetv._tcp.local.",
|
"_mediaremotetv._tcp.local.",
|
||||||
"_companion-link._tcp.local.",
|
"_companion-link._tcp.local.",
|
||||||
|
@@ -36,10 +36,11 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
|||||||
|
|
||||||
async def _async_setup(self) -> None:
|
async def _async_setup(self) -> None:
|
||||||
try:
|
try:
|
||||||
max_power = (await self.api.get_device_info()).maxPower
|
device_info = await self.api.get_device_info()
|
||||||
except (ConnectionError, TimeoutError):
|
except (ConnectionError, TimeoutError):
|
||||||
raise UpdateFailed from None
|
raise UpdateFailed from None
|
||||||
self.api.max_power = max_power
|
self.api.max_power = device_info.maxPower
|
||||||
|
self.api.min_power = device_info.minPower
|
||||||
|
|
||||||
async def _async_update_data(self) -> ApSystemsSensorData:
|
async def _async_update_data(self) -> ApSystemsSensorData:
|
||||||
output_data = await self.api.get_output_data()
|
output_data = await self.api.get_output_data()
|
||||||
|
@@ -26,7 +26,6 @@ async def async_setup_entry(
|
|||||||
class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
|
class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
|
||||||
"""Base sensor to be used with description."""
|
"""Base sensor to be used with description."""
|
||||||
|
|
||||||
_attr_native_min_value = 30
|
|
||||||
_attr_native_step = 1
|
_attr_native_step = 1
|
||||||
_attr_device_class = NumberDeviceClass.POWER
|
_attr_device_class = NumberDeviceClass.POWER
|
||||||
_attr_mode = NumberMode.BOX
|
_attr_mode = NumberMode.BOX
|
||||||
@@ -42,6 +41,7 @@ class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
|
|||||||
self._api = data.coordinator.api
|
self._api = data.coordinator.api
|
||||||
self._attr_unique_id = f"{data.device_id}_output_limit"
|
self._attr_unique_id = f"{data.device_id}_output_limit"
|
||||||
self._attr_native_max_value = data.coordinator.api.max_power
|
self._attr_native_max_value = data.coordinator.api.max_power
|
||||||
|
self._attr_native_min_value = data.coordinator.api.min_power
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Set the state with the value fetched from the inverter."""
|
"""Set the state with the value fetched from the inverter."""
|
||||||
|
@@ -56,7 +56,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
refresh_token = await api.authenticate(
|
refresh_token = await api.authenticate(
|
||||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||||
)
|
)
|
||||||
except ApiException:
|
except (ApiException, TimeoutError):
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except AuthenticationFailed:
|
except AuthenticationFailed:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
|
@@ -56,7 +56,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
|
|||||||
so entities can quickly look up their data.
|
so entities can quickly look up their data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async with asyncio.timeout(10):
|
async with asyncio.timeout(30):
|
||||||
# Check if the refresh token is expired
|
# Check if the refresh token is expired
|
||||||
expiry_time = (
|
expiry_time = (
|
||||||
self.refresh_token_creation_time
|
self.refresh_token_creation_time
|
||||||
@@ -72,7 +72,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
|
|||||||
softeners = await self.aquacell_api.get_all_softeners()
|
softeners = await self.aquacell_api.get_all_softeners()
|
||||||
except AuthenticationFailed as err:
|
except AuthenticationFailed as err:
|
||||||
raise ConfigEntryError from err
|
raise ConfigEntryError from err
|
||||||
except AquacellApiException as err:
|
except (AquacellApiException, TimeoutError) as err:
|
||||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||||
|
|
||||||
return {softener.dsn: softener for softener in softeners}
|
return {softener.dsn: softener for softener in softeners}
|
||||||
|
@@ -4,13 +4,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aioaseko import APIUnavailable, InvalidAuthCredentials, MobileAccount
|
from aioaseko import Aseko, AsekoNotLoggedIn
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AsekoDataUpdateCoordinator
|
from .coordinator import AsekoDataUpdateCoordinator
|
||||||
@@ -22,28 +21,17 @@ PLATFORMS: list[str] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Aseko Pool Live from a config entry."""
|
"""Set up Aseko Pool Live from a config entry."""
|
||||||
account = MobileAccount(
|
aseko = Aseko(entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD])
|
||||||
async_get_clientsession(hass),
|
|
||||||
username=entry.data[CONF_EMAIL],
|
|
||||||
password=entry.data[CONF_PASSWORD],
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
units = await account.get_units()
|
await aseko.login()
|
||||||
except InvalidAuthCredentials as err:
|
except AsekoNotLoggedIn as err:
|
||||||
raise ConfigEntryAuthFailed from err
|
raise ConfigEntryAuthFailed from err
|
||||||
except APIUnavailable as err:
|
|
||||||
raise ConfigEntryNotReady from err
|
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = []
|
|
||||||
|
|
||||||
for unit in units:
|
|
||||||
coordinator = AsekoDataUpdateCoordinator(hass, unit)
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
|
||||||
hass.data[DOMAIN][entry.entry_id].append((unit, coordinator))
|
|
||||||
|
|
||||||
|
coordinator = AsekoDataUpdateCoordinator(hass, aseko)
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -51,7 +39,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
@@ -8,7 +8,6 @@ from dataclasses import dataclass
|
|||||||
from aioaseko import Unit
|
from aioaseko import Unit
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDeviceClass,
|
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
@@ -25,26 +24,14 @@ from .entity import AsekoEntity
|
|||||||
class AsekoBinarySensorEntityDescription(BinarySensorEntityDescription):
|
class AsekoBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||||
"""Describes an Aseko binary sensor entity."""
|
"""Describes an Aseko binary sensor entity."""
|
||||||
|
|
||||||
value_fn: Callable[[Unit], bool]
|
value_fn: Callable[[Unit], bool | None]
|
||||||
|
|
||||||
|
|
||||||
UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
|
BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
|
||||||
AsekoBinarySensorEntityDescription(
|
AsekoBinarySensorEntityDescription(
|
||||||
key="water_flow",
|
key="water_flow",
|
||||||
translation_key="water_flow",
|
translation_key="water_flow_to_probes",
|
||||||
value_fn=lambda unit: unit.water_flow,
|
value_fn=lambda unit: unit.water_flow_to_probes,
|
||||||
),
|
|
||||||
AsekoBinarySensorEntityDescription(
|
|
||||||
key="has_alarm",
|
|
||||||
translation_key="alarm",
|
|
||||||
value_fn=lambda unit: unit.has_alarm,
|
|
||||||
device_class=BinarySensorDeviceClass.SAFETY,
|
|
||||||
),
|
|
||||||
AsekoBinarySensorEntityDescription(
|
|
||||||
key="has_error",
|
|
||||||
translation_key="error",
|
|
||||||
value_fn=lambda unit: unit.has_error,
|
|
||||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,33 +42,22 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Aseko Pool Live binary sensors."""
|
"""Set up the Aseko Pool Live binary sensors."""
|
||||||
data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][
|
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
config_entry.entry_id
|
units = coordinator.data.values()
|
||||||
]
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
AsekoUnitBinarySensorEntity(unit, coordinator, description)
|
AsekoBinarySensorEntity(unit, coordinator, description)
|
||||||
for unit, coordinator in data
|
for description in BINARY_SENSORS
|
||||||
for description in UNIT_BINARY_SENSORS
|
for unit in units
|
||||||
|
if description.value_fn(unit) is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity):
|
class AsekoBinarySensorEntity(AsekoEntity, BinarySensorEntity):
|
||||||
"""Representation of a unit water flow binary sensor entity."""
|
"""Representation of an Aseko binary sensor entity."""
|
||||||
|
|
||||||
entity_description: AsekoBinarySensorEntityDescription
|
entity_description: AsekoBinarySensorEntityDescription
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
unit: Unit,
|
|
||||||
coordinator: AsekoDataUpdateCoordinator,
|
|
||||||
entity_description: AsekoBinarySensorEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the unit binary sensor."""
|
|
||||||
super().__init__(unit, coordinator)
|
|
||||||
self.entity_description = entity_description
|
|
||||||
self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool | None:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self.entity_description.value_fn(self._unit)
|
return self.entity_description.value_fn(self.unit)
|
||||||
|
@@ -6,12 +6,11 @@ from collections.abc import Mapping
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aioaseko import APIUnavailable, InvalidAuthCredentials, WebAccount
|
from aioaseko import Aseko, AsekoAPIError, AsekoInvalidCredentials
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID
|
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
@@ -34,15 +33,12 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
async def get_account_info(self, email: str, password: str) -> dict:
|
async def get_account_info(self, email: str, password: str) -> dict:
|
||||||
"""Get account info from the mobile API and the web API."""
|
"""Get account info from the mobile API and the web API."""
|
||||||
session = async_get_clientsession(self.hass)
|
aseko = Aseko(email, password)
|
||||||
|
user = await aseko.login()
|
||||||
web_account = WebAccount(session, email, password)
|
|
||||||
web_account_info = await web_account.login()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
CONF_EMAIL: email,
|
CONF_EMAIL: email,
|
||||||
CONF_PASSWORD: password,
|
CONF_PASSWORD: password,
|
||||||
CONF_UNIQUE_ID: web_account_info.user_id,
|
CONF_UNIQUE_ID: user.user_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
@@ -58,9 +54,9 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
info = await self.get_account_info(
|
info = await self.get_account_info(
|
||||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||||
)
|
)
|
||||||
except APIUnavailable:
|
except AsekoAPIError:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except InvalidAuthCredentials:
|
except AsekoInvalidCredentials:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
@@ -101,7 +97,7 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_reauth(
|
async def async_step_reauth(
|
||||||
self, user_input: Mapping[str, Any]
|
self, entry_data: Mapping[str, Any]
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Perform reauth upon an API authentication error."""
|
"""Perform reauth upon an API authentication error."""
|
||||||
|
|
||||||
@@ -109,10 +105,10 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self.context["entry_id"]
|
self.context["entry_id"]
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self.async_step_reauth_confirm(user_input)
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
async def async_step_reauth_confirm(
|
||||||
self, user_input: Mapping | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Dialog that informs the user that reauth is required."""
|
"""Dialog that informs the user that reauth is required."""
|
||||||
|
|
||||||
@@ -122,9 +118,9 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
info = await self.get_account_info(
|
info = await self.get_account_info(
|
||||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||||
)
|
)
|
||||||
except APIUnavailable:
|
except AsekoAPIError:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except InvalidAuthCredentials:
|
except AsekoInvalidCredentials:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
@@ -5,34 +5,31 @@ from __future__ import annotations
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aioaseko import Unit, Variable
|
from aioaseko import Aseko, Unit
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Variable]]):
|
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Unit]]):
|
||||||
"""Class to manage fetching Aseko unit data from single endpoint."""
|
"""Class to manage fetching Aseko unit data from single endpoint."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, unit: Unit) -> None:
|
def __init__(self, hass: HomeAssistant, aseko: Aseko) -> None:
|
||||||
"""Initialize global Aseko unit data updater."""
|
"""Initialize global Aseko unit data updater."""
|
||||||
self._unit = unit
|
self._aseko = aseko
|
||||||
|
|
||||||
if self._unit.name:
|
|
||||||
name = self._unit.name
|
|
||||||
else:
|
|
||||||
name = f"{self._unit.type}-{self._unit.serial_number}"
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
name=name,
|
name=DOMAIN,
|
||||||
update_interval=timedelta(minutes=2),
|
update_interval=timedelta(minutes=2),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, Variable]:
|
async def _async_update_data(self) -> dict[str, Unit]:
|
||||||
"""Fetch unit data."""
|
"""Fetch unit data."""
|
||||||
await self._unit.get_state()
|
units = await self._aseko.get_units()
|
||||||
return {variable.type: variable for variable in self._unit.variables}
|
return {unit.serial_number: unit for unit in units}
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
from aioaseko import Unit
|
from aioaseko import Unit
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@@ -14,20 +15,44 @@ class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]):
|
|||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
unit: Unit,
|
||||||
|
coordinator: AsekoDataUpdateCoordinator,
|
||||||
|
description: EntityDescription,
|
||||||
|
) -> None:
|
||||||
"""Initialize the aseko entity."""
|
"""Initialize the aseko entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
self.entity_description = description
|
||||||
self._unit = unit
|
self._unit = unit
|
||||||
|
self._attr_unique_id = f"{self.unit.serial_number}{self.entity_description.key}"
|
||||||
if self._unit.type == "Remote":
|
|
||||||
self._device_model = "ASIN Pool"
|
|
||||||
else:
|
|
||||||
self._device_model = f"ASIN AQUA {self._unit.type}"
|
|
||||||
self._device_name = self._unit.name if self._unit.name else self._device_model
|
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
name=self._device_name,
|
identifiers={(DOMAIN, self.unit.serial_number)},
|
||||||
identifiers={(DOMAIN, str(self._unit.serial_number))},
|
serial_number=self.unit.serial_number,
|
||||||
manufacturer="Aseko",
|
name=unit.name or unit.serial_number,
|
||||||
model=self._device_model,
|
manufacturer=(
|
||||||
|
self.unit.brand_name.primary
|
||||||
|
if self.unit.brand_name is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
model=(
|
||||||
|
self.unit.brand_name.secondary
|
||||||
|
if self.unit.brand_name is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
configuration_url=f"https://aseko.cloud/unit/{self.unit.serial_number}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit(self) -> Unit:
|
||||||
|
"""Return the aseko unit."""
|
||||||
|
return self.coordinator.data[self._unit.serial_number]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return (
|
||||||
|
super().available
|
||||||
|
and self.unit.serial_number in self.coordinator.data
|
||||||
|
and self.unit.online
|
||||||
)
|
)
|
||||||
|
@@ -1,16 +1,25 @@
|
|||||||
{
|
{
|
||||||
"entity": {
|
"entity": {
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"water_flow": {
|
"water_flow_to_probes": {
|
||||||
"default": "mdi:waves-arrow-right"
|
"default": "mdi:waves-arrow-right"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
"air_temperature": {
|
||||||
|
"default": "mdi:thermometer-lines"
|
||||||
|
},
|
||||||
"free_chlorine": {
|
"free_chlorine": {
|
||||||
"default": "mdi:flask"
|
"default": "mdi:pool"
|
||||||
|
},
|
||||||
|
"redox": {
|
||||||
|
"default": "mdi:pool"
|
||||||
|
},
|
||||||
|
"salinity": {
|
||||||
|
"default": "mdi:pool"
|
||||||
},
|
},
|
||||||
"water_temperature": {
|
"water_temperature": {
|
||||||
"default": "mdi:coolant-temperature"
|
"default": "mdi:pool-thermometer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
|
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioaseko"],
|
"loggers": ["aioaseko"],
|
||||||
"requirements": ["aioaseko==0.2.0"]
|
"requirements": ["aioaseko==1.0.0"]
|
||||||
}
|
}
|
||||||
|
@@ -2,77 +2,104 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from aioaseko import Unit, Variable
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from aioaseko import Unit
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import UnitOfElectricPotential, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AsekoDataUpdateCoordinator
|
from .coordinator import AsekoDataUpdateCoordinator
|
||||||
from .entity import AsekoEntity
|
from .entity import AsekoEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AsekoSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes an Aseko sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[Unit], StateType]
|
||||||
|
|
||||||
|
|
||||||
|
SENSORS: list[AsekoSensorEntityDescription] = [
|
||||||
|
AsekoSensorEntityDescription(
|
||||||
|
key="airTemp",
|
||||||
|
translation_key="air_temperature",
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda unit: unit.air_temperature,
|
||||||
|
),
|
||||||
|
AsekoSensorEntityDescription(
|
||||||
|
key="free_chlorine",
|
||||||
|
translation_key="free_chlorine",
|
||||||
|
native_unit_of_measurement="mg/l",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda unit: unit.cl_free,
|
||||||
|
),
|
||||||
|
AsekoSensorEntityDescription(
|
||||||
|
key="ph",
|
||||||
|
device_class=SensorDeviceClass.PH,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda unit: unit.ph,
|
||||||
|
),
|
||||||
|
AsekoSensorEntityDescription(
|
||||||
|
key="rx",
|
||||||
|
translation_key="redox",
|
||||||
|
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda unit: unit.redox,
|
||||||
|
),
|
||||||
|
AsekoSensorEntityDescription(
|
||||||
|
key="salinity",
|
||||||
|
translation_key="salinity",
|
||||||
|
native_unit_of_measurement="kg/m³",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda unit: unit.salinity,
|
||||||
|
),
|
||||||
|
AsekoSensorEntityDescription(
|
||||||
|
key="waterTemp",
|
||||||
|
translation_key="water_temperature",
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda unit: unit.water_temperature,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Aseko Pool Live sensors."""
|
"""Set up the Aseko Pool Live sensors."""
|
||||||
data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][
|
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
config_entry.entry_id
|
units = coordinator.data.values()
|
||||||
]
|
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
VariableSensorEntity(unit, variable, coordinator)
|
AsekoSensorEntity(unit, coordinator, description)
|
||||||
for unit, coordinator in data
|
for description in SENSORS
|
||||||
for variable in unit.variables
|
for unit in units
|
||||||
|
if description.value_fn(unit) is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class VariableSensorEntity(AsekoEntity, SensorEntity):
|
class AsekoSensorEntity(AsekoEntity, SensorEntity):
|
||||||
"""Representation of a unit variable sensor entity."""
|
"""Representation of an Aseko unit sensor entity."""
|
||||||
|
|
||||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
entity_description: AsekoSensorEntityDescription
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, unit: Unit, variable: Variable, coordinator: AsekoDataUpdateCoordinator
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the variable sensor."""
|
|
||||||
super().__init__(unit, coordinator)
|
|
||||||
self._variable = variable
|
|
||||||
|
|
||||||
translation_key = {
|
|
||||||
"Air temp.": "air_temperature",
|
|
||||||
"Cl free": "free_chlorine",
|
|
||||||
"Water temp.": "water_temperature",
|
|
||||||
}.get(self._variable.name)
|
|
||||||
if translation_key is not None:
|
|
||||||
self._attr_translation_key = translation_key
|
|
||||||
else:
|
|
||||||
self._attr_name = self._variable.name
|
|
||||||
|
|
||||||
self._attr_unique_id = f"{self._unit.serial_number}{self._variable.type}"
|
|
||||||
self._attr_native_unit_of_measurement = self._variable.unit
|
|
||||||
|
|
||||||
self._attr_icon = {
|
|
||||||
"rx": "mdi:test-tube",
|
|
||||||
"waterLevel": "mdi:waves",
|
|
||||||
}.get(self._variable.type)
|
|
||||||
|
|
||||||
self._attr_device_class = {
|
|
||||||
"airTemp": SensorDeviceClass.TEMPERATURE,
|
|
||||||
"waterTemp": SensorDeviceClass.TEMPERATURE,
|
|
||||||
"ph": SensorDeviceClass.PH,
|
|
||||||
}.get(self._variable.type)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> int | None:
|
def native_value(self) -> StateType:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
variable = self.coordinator.data[self._variable.type]
|
return self.entity_description.value_fn(self.unit)
|
||||||
return variable.current_value
|
|
||||||
|
@@ -26,11 +26,8 @@
|
|||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"water_flow": {
|
"water_flow_to_probes": {
|
||||||
"name": "Water flow"
|
"name": "Water flow to probes"
|
||||||
},
|
|
||||||
"alarm": {
|
|
||||||
"name": "Alarm"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
@@ -40,6 +37,12 @@
|
|||||||
"free_chlorine": {
|
"free_chlorine": {
|
||||||
"name": "Free chlorine"
|
"name": "Free chlorine"
|
||||||
},
|
},
|
||||||
|
"redox": {
|
||||||
|
"name": "Redox potential"
|
||||||
|
},
|
||||||
|
"salinity": {
|
||||||
|
"name": "Salinity"
|
||||||
|
},
|
||||||
"water_temperature": {
|
"water_temperature": {
|
||||||
"name": "Water temperature"
|
"name": "Water temperature"
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pymicro_vad import MicroVad
|
from pymicro_vad import MicroVad
|
||||||
|
from pyspeex_noise import AudioProcessor
|
||||||
|
|
||||||
from .const import BYTES_PER_CHUNK
|
from .const import BYTES_PER_CHUNK
|
||||||
|
|
||||||
@@ -41,8 +42,8 @@ class AudioEnhancer(ABC):
|
|||||||
"""Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
"""Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||||
|
|
||||||
|
|
||||||
class MicroVadEnhancer(AudioEnhancer):
|
class MicroVadSpeexEnhancer(AudioEnhancer):
|
||||||
"""Audio enhancer that just runs microVAD."""
|
"""Audio enhancer that runs microVAD and speex."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool
|
self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool
|
||||||
@@ -50,6 +51,24 @@ class MicroVadEnhancer(AudioEnhancer):
|
|||||||
"""Initialize audio enhancer."""
|
"""Initialize audio enhancer."""
|
||||||
super().__init__(auto_gain, noise_suppression, is_vad_enabled)
|
super().__init__(auto_gain, noise_suppression, is_vad_enabled)
|
||||||
|
|
||||||
|
self.audio_processor: AudioProcessor | None = None
|
||||||
|
|
||||||
|
# Scale from 0-4
|
||||||
|
self.noise_suppression = noise_suppression * -15
|
||||||
|
|
||||||
|
# Scale from 0-31
|
||||||
|
self.auto_gain = auto_gain * 300
|
||||||
|
|
||||||
|
if (self.auto_gain != 0) or (self.noise_suppression != 0):
|
||||||
|
self.audio_processor = AudioProcessor(
|
||||||
|
self.auto_gain, self.noise_suppression
|
||||||
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Initialized speex with auto_gain=%s, noise_suppression=%s",
|
||||||
|
self.auto_gain,
|
||||||
|
self.noise_suppression,
|
||||||
|
)
|
||||||
|
|
||||||
self.vad: MicroVad | None = None
|
self.vad: MicroVad | None = None
|
||||||
self.threshold = 0.5
|
self.threshold = 0.5
|
||||||
|
|
||||||
@@ -61,12 +80,17 @@ class MicroVadEnhancer(AudioEnhancer):
|
|||||||
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||||
is_speech: bool | None = None
|
is_speech: bool | None = None
|
||||||
|
|
||||||
|
assert len(audio) == BYTES_PER_CHUNK
|
||||||
|
|
||||||
if self.vad is not None:
|
if self.vad is not None:
|
||||||
# Run VAD
|
# Run VAD
|
||||||
assert len(audio) == BYTES_PER_CHUNK
|
|
||||||
speech_prob = self.vad.Process10ms(audio)
|
speech_prob = self.vad.Process10ms(audio)
|
||||||
is_speech = speech_prob > self.threshold
|
is_speech = speech_prob > self.threshold
|
||||||
|
|
||||||
|
if self.audio_processor is not None:
|
||||||
|
# Run noise suppression and auto gain
|
||||||
|
audio = self.audio_processor.Process10ms(audio).audio
|
||||||
|
|
||||||
return EnhancedAudioChunk(
|
return EnhancedAudioChunk(
|
||||||
audio=audio, timestamp_ms=timestamp_ms, is_speech=is_speech
|
audio=audio, timestamp_ms=timestamp_ms, is_speech=is_speech
|
||||||
)
|
)
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["pymicro-vad==1.0.1"]
|
"requirements": ["pymicro-vad==1.0.1", "pyspeex-noise==1.0.2"]
|
||||||
}
|
}
|
||||||
|
@@ -49,7 +49,7 @@ from homeassistant.util import (
|
|||||||
)
|
)
|
||||||
from homeassistant.util.limited_size_dict import LimitedSizeDict
|
from homeassistant.util.limited_size_dict import LimitedSizeDict
|
||||||
|
|
||||||
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadEnhancer
|
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadSpeexEnhancer
|
||||||
from .const import (
|
from .const import (
|
||||||
BYTES_PER_CHUNK,
|
BYTES_PER_CHUNK,
|
||||||
CONF_DEBUG_RECORDING_DIR,
|
CONF_DEBUG_RECORDING_DIR,
|
||||||
@@ -589,7 +589,7 @@ class PipelineRun:
|
|||||||
# Initialize with audio settings
|
# Initialize with audio settings
|
||||||
if self.audio_settings.needs_processor and (self.audio_enhancer is None):
|
if self.audio_settings.needs_processor and (self.audio_enhancer is None):
|
||||||
# Default audio enhancer
|
# Default audio enhancer
|
||||||
self.audio_enhancer = MicroVadEnhancer(
|
self.audio_enhancer = MicroVadSpeexEnhancer(
|
||||||
self.audio_settings.auto_gain_dbfs,
|
self.audio_settings.auto_gain_dbfs,
|
||||||
self.audio_settings.noise_suppression_level,
|
self.audio_settings.noise_suppression_level,
|
||||||
self.audio_settings.is_vad_enabled,
|
self.audio_settings.is_vad_enabled,
|
||||||
|
@@ -78,6 +78,9 @@ class VoiceCommandSegmenter:
|
|||||||
speech_seconds: float = 0.3
|
speech_seconds: float = 0.3
|
||||||
"""Seconds of speech before voice command has started."""
|
"""Seconds of speech before voice command has started."""
|
||||||
|
|
||||||
|
command_seconds: float = 1.0
|
||||||
|
"""Minimum number of seconds for a voice command."""
|
||||||
|
|
||||||
silence_seconds: float = 0.7
|
silence_seconds: float = 0.7
|
||||||
"""Seconds of silence after voice command has ended."""
|
"""Seconds of silence after voice command has ended."""
|
||||||
|
|
||||||
@@ -96,6 +99,9 @@ class VoiceCommandSegmenter:
|
|||||||
_speech_seconds_left: float = 0.0
|
_speech_seconds_left: float = 0.0
|
||||||
"""Seconds left before considering voice command as started."""
|
"""Seconds left before considering voice command as started."""
|
||||||
|
|
||||||
|
_command_seconds_left: float = 0.0
|
||||||
|
"""Seconds left before voice command could stop."""
|
||||||
|
|
||||||
_silence_seconds_left: float = 0.0
|
_silence_seconds_left: float = 0.0
|
||||||
"""Seconds left before considering voice command as stopped."""
|
"""Seconds left before considering voice command as stopped."""
|
||||||
|
|
||||||
@@ -112,6 +118,7 @@ class VoiceCommandSegmenter:
|
|||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""Reset all counters and state."""
|
"""Reset all counters and state."""
|
||||||
self._speech_seconds_left = self.speech_seconds
|
self._speech_seconds_left = self.speech_seconds
|
||||||
|
self._command_seconds_left = self.command_seconds - self.speech_seconds
|
||||||
self._silence_seconds_left = self.silence_seconds
|
self._silence_seconds_left = self.silence_seconds
|
||||||
self._timeout_seconds_left = self.timeout_seconds
|
self._timeout_seconds_left = self.timeout_seconds
|
||||||
self._reset_seconds_left = self.reset_seconds
|
self._reset_seconds_left = self.reset_seconds
|
||||||
@@ -142,6 +149,9 @@ class VoiceCommandSegmenter:
|
|||||||
if self._speech_seconds_left <= 0:
|
if self._speech_seconds_left <= 0:
|
||||||
# Inside voice command
|
# Inside voice command
|
||||||
self.in_command = True
|
self.in_command = True
|
||||||
|
self._command_seconds_left = (
|
||||||
|
self.command_seconds - self.speech_seconds
|
||||||
|
)
|
||||||
self._silence_seconds_left = self.silence_seconds
|
self._silence_seconds_left = self.silence_seconds
|
||||||
_LOGGER.debug("Voice command started")
|
_LOGGER.debug("Voice command started")
|
||||||
else:
|
else:
|
||||||
@@ -154,7 +164,8 @@ class VoiceCommandSegmenter:
|
|||||||
# Silence in command
|
# Silence in command
|
||||||
self._reset_seconds_left = self.reset_seconds
|
self._reset_seconds_left = self.reset_seconds
|
||||||
self._silence_seconds_left -= chunk_seconds
|
self._silence_seconds_left -= chunk_seconds
|
||||||
if self._silence_seconds_left <= 0:
|
self._command_seconds_left -= chunk_seconds
|
||||||
|
if (self._silence_seconds_left <= 0) and (self._command_seconds_left <= 0):
|
||||||
# Command finished successfully
|
# Command finished successfully
|
||||||
self.reset()
|
self.reset()
|
||||||
_LOGGER.debug("Voice command finished")
|
_LOGGER.debug("Voice command finished")
|
||||||
@@ -163,6 +174,7 @@ class VoiceCommandSegmenter:
|
|||||||
# Speech in command.
|
# Speech in command.
|
||||||
# Reset silence counter if enough speech.
|
# Reset silence counter if enough speech.
|
||||||
self._reset_seconds_left -= chunk_seconds
|
self._reset_seconds_left -= chunk_seconds
|
||||||
|
self._command_seconds_left -= chunk_seconds
|
||||||
if self._reset_seconds_left <= 0:
|
if self._reset_seconds_left <= 0:
|
||||||
self._silence_seconds_left = self.silence_seconds
|
self._silence_seconds_left = self.silence_seconds
|
||||||
self._reset_seconds_left = self.reset_seconds
|
self._reset_seconds_left = self.reset_seconds
|
||||||
|
@@ -6,15 +6,16 @@ from pathlib import Path
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from aiohttp import ClientResponseError
|
from aiohttp import ClientResponseError
|
||||||
|
from yalexs.const import Brand
|
||||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||||
from yalexs.manager.gateway import Config as YaleXSConfig
|
from yalexs.manager.gateway import Config as YaleXSConfig
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||||
|
|
||||||
from .const import DOMAIN, PLATFORMS
|
from .const import DOMAIN, PLATFORMS
|
||||||
from .data import AugustData
|
from .data import AugustData
|
||||||
@@ -24,7 +25,27 @@ from .util import async_create_august_clientsession
|
|||||||
type AugustConfigEntry = ConfigEntry[AugustData]
|
type AugustConfigEntry = ConfigEntry[AugustData]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
@callback
|
||||||
|
def _async_create_yale_brand_migration_issue(
|
||||||
|
hass: HomeAssistant, entry: AugustConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Create an issue for a brand migration."""
|
||||||
|
ir.async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
"yale_brand_migration",
|
||||||
|
breaks_in_ha_version="2024.9",
|
||||||
|
learn_more_url="https://www.home-assistant.io/integrations/yale",
|
||||||
|
translation_key="yale_brand_migration",
|
||||||
|
is_fixable=False,
|
||||||
|
severity=ir.IssueSeverity.CRITICAL,
|
||||||
|
translation_placeholders={
|
||||||
|
"migrate_url": "https://my.home-assistant.io/redirect/config_flow_start?domain=yale"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool:
|
||||||
"""Set up August from a config entry."""
|
"""Set up August from a config entry."""
|
||||||
session = async_create_august_clientsession(hass)
|
session = async_create_august_clientsession(hass)
|
||||||
august_gateway = AugustGateway(Path(hass.config.config_dir), session)
|
august_gateway = AugustGateway(Path(hass.config.config_dir), session)
|
||||||
@@ -40,6 +61,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_remove_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> None:
|
||||||
|
"""Remove an August config entry."""
|
||||||
|
ir.async_delete_issue(hass, DOMAIN, "yale_brand_migration")
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
@@ -51,6 +77,8 @@ async def async_setup_august(
|
|||||||
"""Set up the August component."""
|
"""Set up the August component."""
|
||||||
config = cast(YaleXSConfig, entry.data)
|
config = cast(YaleXSConfig, entry.data)
|
||||||
await august_gateway.async_setup(config)
|
await august_gateway.async_setup(config)
|
||||||
|
if august_gateway.api.brand == Brand.YALE_HOME:
|
||||||
|
_async_create_yale_brand_migration_issue(hass, entry)
|
||||||
await august_gateway.async_authenticate()
|
await august_gateway.async_authenticate()
|
||||||
await august_gateway.async_refresh_access_token_if_needed()
|
await august_gateway.async_refresh_access_token_if_needed()
|
||||||
data = entry.runtime_data = AugustData(hass, august_gateway)
|
data = entry.runtime_data = AugustData(hass, august_gateway)
|
||||||
|
@@ -109,12 +109,11 @@ async def async_setup_entry(
|
|||||||
for description in SENSOR_TYPES_DOORBELL
|
for description in SENSOR_TYPES_DOORBELL
|
||||||
)
|
)
|
||||||
|
|
||||||
for doorbell in data.doorbells:
|
entities.extend(
|
||||||
entities.extend(
|
AugustDoorbellBinarySensor(data, doorbell, description)
|
||||||
AugustDoorbellBinarySensor(data, doorbell, description)
|
for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL
|
||||||
for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL
|
for doorbell in data.doorbells
|
||||||
)
|
)
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AugustConfigEntry
|
from . import AugustConfigEntry
|
||||||
from .entity import AugustEntityMixin
|
from .entity import AugustEntity
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -18,7 +18,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(AugustWakeLockButton(data, lock, "wake") for lock in data.locks)
|
async_add_entities(AugustWakeLockButton(data, lock, "wake") for lock in data.locks)
|
||||||
|
|
||||||
|
|
||||||
class AugustWakeLockButton(AugustEntityMixin, ButtonEntity):
|
class AugustWakeLockButton(AugustEntity, ButtonEntity):
|
||||||
"""Representation of an August lock wake button."""
|
"""Representation of an August lock wake button."""
|
||||||
|
|
||||||
_attr_translation_key = "wake"
|
_attr_translation_key = "wake"
|
||||||
|
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
|
|
||||||
from . import AugustConfigEntry, AugustData
|
from . import AugustConfigEntry, AugustData
|
||||||
from .const import DEFAULT_NAME, DEFAULT_TIMEOUT
|
from .const import DEFAULT_NAME, DEFAULT_TIMEOUT
|
||||||
from .entity import AugustEntityMixin
|
from .entity import AugustEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AugustCamera(AugustEntityMixin, Camera):
|
class AugustCamera(AugustEntity, Camera):
|
||||||
"""An implementation of an August security camera."""
|
"""An implementation of an August security camera."""
|
||||||
|
|
||||||
_attr_translation_key = "camera"
|
_attr_translation_key = "camera"
|
||||||
|
@@ -9,7 +9,7 @@ from typing import Any
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from yalexs.authenticator_common import ValidationResult
|
from yalexs.authenticator_common import ValidationResult
|
||||||
from yalexs.const import BRANDS, DEFAULT_BRAND
|
from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND, Brand
|
||||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
@@ -28,6 +28,12 @@ from .const import (
|
|||||||
from .gateway import AugustGateway
|
from .gateway import AugustGateway
|
||||||
from .util import async_create_august_clientsession
|
from .util import async_create_august_clientsession
|
||||||
|
|
||||||
|
# The Yale Home Brand is not supported by the August integration
|
||||||
|
# anymore and should migrate to the Yale integration
|
||||||
|
AVAILABLE_BRANDS = BRANDS_WITHOUT_OAUTH.copy()
|
||||||
|
del AVAILABLE_BRANDS[Brand.YALE_HOME]
|
||||||
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -118,7 +124,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_BRAND,
|
CONF_BRAND,
|
||||||
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
|
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
|
||||||
): vol.In(BRANDS),
|
): vol.In(AVAILABLE_BRANDS),
|
||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_LOGIN_METHOD,
|
CONF_LOGIN_METHOD,
|
||||||
default=self._user_auth_details.get(
|
default=self._user_auth_details.get(
|
||||||
@@ -208,7 +214,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_BRAND,
|
CONF_BRAND,
|
||||||
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
|
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
|
||||||
): vol.In(BRANDS),
|
): vol.In(BRANDS_WITHOUT_OAUTH),
|
||||||
vol.Required(CONF_PASSWORD): str,
|
vol.Required(CONF_PASSWORD): str,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@@ -20,7 +20,7 @@ from .const import MANUFACTURER
|
|||||||
DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"]
|
DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"]
|
||||||
|
|
||||||
|
|
||||||
class AugustEntityMixin(Entity):
|
class AugustEntity(Entity):
|
||||||
"""Base implementation for August device."""
|
"""Base implementation for August device."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
@@ -87,7 +87,7 @@ class AugustEntityMixin(Entity):
|
|||||||
self._update_from_data()
|
self._update_from_data()
|
||||||
|
|
||||||
|
|
||||||
class AugustDescriptionEntity(AugustEntityMixin):
|
class AugustDescriptionEntity(AugustEntity):
|
||||||
"""An August entity with a description."""
|
"""An August entity with a description."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@@ -63,22 +63,17 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the august event platform."""
|
"""Set up the august event platform."""
|
||||||
data = config_entry.runtime_data
|
data = config_entry.runtime_data
|
||||||
entities: list[AugustEventEntity] = []
|
entities: list[AugustEventEntity] = [
|
||||||
|
AugustEventEntity(data, lock, description)
|
||||||
for lock in data.locks:
|
for description in TYPES_DOORBELL
|
||||||
detail = data.get_device_detail(lock.device_id)
|
for lock in data.locks
|
||||||
if detail.doorbell:
|
if (detail := data.get_device_detail(lock.device_id)) and detail.doorbell
|
||||||
entities.extend(
|
]
|
||||||
AugustEventEntity(data, lock, description)
|
entities.extend(
|
||||||
for description in TYPES_DOORBELL
|
AugustEventEntity(data, doorbell, description)
|
||||||
)
|
for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL
|
||||||
|
for doorbell in data.doorbells
|
||||||
for doorbell in data.doorbells:
|
)
|
||||||
entities.extend(
|
|
||||||
AugustEventEntity(data, doorbell, description)
|
|
||||||
for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
@@ -86,7 +81,6 @@ class AugustEventEntity(AugustDescriptionEntity, EventEntity):
|
|||||||
"""An august event entity."""
|
"""An august event entity."""
|
||||||
|
|
||||||
entity_description: AugustEventEntityDescription
|
entity_description: AugustEventEntityDescription
|
||||||
_attr_has_entity_name = True
|
|
||||||
_last_activity: Activity | None = None
|
_last_activity: Activity | None = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@@ -19,7 +19,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
|||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from . import AugustConfigEntry, AugustData
|
from . import AugustConfigEntry, AugustData
|
||||||
from .entity import AugustEntityMixin
|
from .entity import AugustEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(AugustLock(data, lock) for lock in data.locks)
|
async_add_entities(AugustLock(data, lock) for lock in data.locks)
|
||||||
|
|
||||||
|
|
||||||
class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
|
class AugustLock(AugustEntity, RestoreEntity, LockEntity):
|
||||||
"""Representation of an August lock."""
|
"""Representation of an August lock."""
|
||||||
|
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
|
@@ -4,10 +4,6 @@
|
|||||||
"codeowners": ["@bdraco"],
|
"codeowners": ["@bdraco"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dhcp": [
|
"dhcp": [
|
||||||
{
|
|
||||||
"hostname": "yale-connect-plus",
|
|
||||||
"macaddress": "00177A*"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"hostname": "connect",
|
"hostname": "connect",
|
||||||
"macaddress": "D86162*"
|
"macaddress": "D86162*"
|
||||||
@@ -28,5 +24,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pubnub", "yalexs"],
|
"loggers": ["pubnub", "yalexs"],
|
||||||
"requirements": ["yalexs==8.1.4", "yalexs-ble==2.4.3"]
|
"requirements": ["yalexs==8.6.4", "yalexs-ble==2.4.3"]
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Generic, TypeVar, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from yalexs.activity import ActivityType, LockOperationActivity
|
from yalexs.activity import ActivityType, LockOperationActivity
|
||||||
from yalexs.doorbell import Doorbell
|
from yalexs.doorbell import Doorbell
|
||||||
@@ -42,7 +42,7 @@ from .const import (
|
|||||||
OPERATION_METHOD_REMOTE,
|
OPERATION_METHOD_REMOTE,
|
||||||
OPERATION_METHOD_TAG,
|
OPERATION_METHOD_TAG,
|
||||||
)
|
)
|
||||||
from .entity import AugustDescriptionEntity, AugustEntityMixin
|
from .entity import AugustDescriptionEntity, AugustEntity
|
||||||
|
|
||||||
|
|
||||||
def _retrieve_device_battery_state(detail: LockDetail) -> int:
|
def _retrieve_device_battery_state(detail: LockDetail) -> int:
|
||||||
@@ -55,14 +55,13 @@ def _retrieve_linked_keypad_battery_state(detail: KeypadDetail) -> int | None:
|
|||||||
return detail.battery_percentage
|
return detail.battery_percentage
|
||||||
|
|
||||||
|
|
||||||
_T = TypeVar("_T", LockDetail, KeypadDetail)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AugustSensorEntityDescription(SensorEntityDescription, Generic[_T]):
|
class AugustSensorEntityDescription[T: LockDetail | KeypadDetail](
|
||||||
|
SensorEntityDescription
|
||||||
|
):
|
||||||
"""Mixin for required keys."""
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
value_fn: Callable[[_T], int | None]
|
value_fn: Callable[[T], int | None]
|
||||||
|
|
||||||
|
|
||||||
SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail](
|
SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail](
|
||||||
@@ -114,7 +113,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class AugustOperatorSensor(AugustEntityMixin, RestoreSensor):
|
class AugustOperatorSensor(AugustEntity, RestoreSensor):
|
||||||
"""Representation of an August lock operation sensor."""
|
"""Representation of an August lock operation sensor."""
|
||||||
|
|
||||||
_attr_translation_key = "operator"
|
_attr_translation_key = "operator"
|
||||||
@@ -198,10 +197,12 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreSensor):
|
|||||||
self._operated_autorelock = last_attrs[ATTR_OPERATION_AUTORELOCK]
|
self._operated_autorelock = last_attrs[ATTR_OPERATION_AUTORELOCK]
|
||||||
|
|
||||||
|
|
||||||
class AugustBatterySensor(AugustDescriptionEntity, SensorEntity, Generic[_T]):
|
class AugustBatterySensor[T: LockDetail | KeypadDetail](
|
||||||
|
AugustDescriptionEntity, SensorEntity
|
||||||
|
):
|
||||||
"""Representation of an August sensor."""
|
"""Representation of an August sensor."""
|
||||||
|
|
||||||
entity_description: AugustSensorEntityDescription[_T]
|
entity_description: AugustSensorEntityDescription[T]
|
||||||
_attr_device_class = SensorDeviceClass.BATTERY
|
_attr_device_class = SensorDeviceClass.BATTERY
|
||||||
_attr_native_unit_of_measurement = PERCENTAGE
|
_attr_native_unit_of_measurement = PERCENTAGE
|
||||||
|
|
||||||
|
@@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
|
"issues": {
|
||||||
|
"yale_brand_migration": {
|
||||||
|
"title": "Yale Home has a new integration",
|
||||||
|
"description": "Add the [Yale integration]({migrate_url}), and remove the August integration as soon as possible to avoid an interruption in service. The Yale Home brand will stop working with the August integration soon and will be removed in a future release."
|
||||||
|
}
|
||||||
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"error": {
|
"error": {
|
||||||
"unhandled": "Unhandled error: {error}",
|
"unhandled": "Unhandled error: {error}",
|
||||||
|
@@ -63,16 +63,11 @@ def _activity_time_based(latest: Activity) -> Activity | None:
|
|||||||
"""Get the latest state of the sensor."""
|
"""Get the latest state of the sensor."""
|
||||||
start = latest.activity_start_time
|
start = latest.activity_start_time
|
||||||
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
|
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
|
||||||
if start <= _native_datetime() <= end:
|
if start <= datetime.now() <= end:
|
||||||
return latest
|
return latest
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _native_datetime() -> datetime:
|
|
||||||
"""Return time in the format august uses without timezone."""
|
|
||||||
return datetime.now()
|
|
||||||
|
|
||||||
|
|
||||||
def retrieve_online_state(
|
def retrieve_online_state(
|
||||||
data: AugustData, detail: DoorbellDetail | LockDetail
|
data: AugustData, detail: DoorbellDetail | LockDetail
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
@@ -75,11 +75,10 @@ class AuroraABBConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
"""Initialise the config flow."""
|
"""Initialise the config flow."""
|
||||||
self.config = None
|
|
||||||
self._com_ports_list: list[str] | None = None
|
self._com_ports_list: list[str] | None = None
|
||||||
self._default_com_port = None
|
self._default_com_port: str | None = None
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
@@ -22,11 +22,11 @@ class AussieBroadbandConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
"""Initialize the config flow."""
|
"""Initialize the config flow."""
|
||||||
self.data: dict = {}
|
self.data: dict = {}
|
||||||
self.options: dict = {CONF_SERVICES: []}
|
self.options: dict = {CONF_SERVICES: []}
|
||||||
self.services: list[dict[str]] = []
|
self.services: list[dict[str, Any]] = []
|
||||||
self.client: AussieBB | None = None
|
self.client: AussieBB | None = None
|
||||||
self._reauth_username: str | None = None
|
self._reauth_username: str | None = None
|
||||||
|
|
||||||
@@ -99,15 +99,11 @@ class AussieBroadbandConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if not (errors := await self.async_auth(data)):
|
if not (errors := await self.async_auth(data)):
|
||||||
entry = await self.async_set_unique_id(self._reauth_username.lower())
|
entry = self.hass.config_entries.async_get_entry(
|
||||||
if entry:
|
self.context["entry_id"]
|
||||||
self.hass.config_entries.async_update_entry(
|
)
|
||||||
entry,
|
assert entry
|
||||||
data=data,
|
return self.async_update_reload_and_abort(entry, data=data)
|
||||||
)
|
|
||||||
await self.hass.config_entries.async_reload(entry.entry_id)
|
|
||||||
return self.async_abort(reason="reauth_successful")
|
|
||||||
return self.async_create_entry(title=self._reauth_username, data=data)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="reauth_confirm",
|
step_id="reauth_confirm",
|
||||||
|
@@ -10,8 +10,10 @@ blueprint:
|
|||||||
selector:
|
selector:
|
||||||
entity:
|
entity:
|
||||||
filter:
|
filter:
|
||||||
device_class: motion
|
- device_class: occupancy
|
||||||
domain: binary_sensor
|
domain: binary_sensor
|
||||||
|
- device_class: motion
|
||||||
|
domain: binary_sensor
|
||||||
light_target:
|
light_target:
|
||||||
name: Light
|
name: Light
|
||||||
selector:
|
selector:
|
||||||
|
@@ -9,10 +9,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"turn_on": "mdi:robot",
|
"turn_on": {
|
||||||
"turn_off": "mdi:robot-off",
|
"service": "mdi:robot"
|
||||||
"toggle": "mdi:robot",
|
},
|
||||||
"trigger": "mdi:robot",
|
"turn_off": {
|
||||||
"reload": "mdi:reload"
|
"service": "mdi:robot-off"
|
||||||
|
},
|
||||||
|
"toggle": {
|
||||||
|
"service": "mdi:robot"
|
||||||
|
},
|
||||||
|
"trigger": {
|
||||||
|
"service": "mdi:robot"
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"service": "mdi:reload"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
"""Config flow for AWS component."""
|
"""Config flow for AWS component."""
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
@@ -13,11 +12,9 @@ class AWSFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
async def async_step_import(
|
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||||
self, user_input: Mapping[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Import a config entry."""
|
"""Import a config entry."""
|
||||||
if self._async_current_entries():
|
if self._async_current_entries():
|
||||||
return self.async_abort(reason="single_instance_allowed")
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
|
|
||||||
return self.async_create_entry(title="configuration.yaml", data=user_input)
|
return self.async_create_entry(title="configuration.yaml", data=import_data)
|
||||||
|
@@ -6,7 +6,7 @@ import logging
|
|||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from aioazuredevops.client import DevOpsClient
|
from aioazuredevops.client import DevOpsClient
|
||||||
from aioazuredevops.models.builds import Build
|
from aioazuredevops.models.build import Build
|
||||||
from aioazuredevops.models.core import Project
|
from aioazuredevops.models.core import Project
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from aioazuredevops.models.builds import Build
|
from aioazuredevops.models.build import Build
|
||||||
from aioazuredevops.models.core import Project
|
from aioazuredevops.models.core import Project
|
||||||
|
|
||||||
|
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/azure_devops",
|
"documentation": "https://www.home-assistant.io/integrations/azure_devops",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioazuredevops"],
|
"loggers": ["aioazuredevops"],
|
||||||
"requirements": ["aioazuredevops==2.1.1"]
|
"requirements": ["aioazuredevops==2.2.1"]
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ from datetime import datetime
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aioazuredevops.models.builds import Build
|
from aioazuredevops.models.build import Build
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
|
@@ -154,17 +154,15 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
options=self._options,
|
options=self._options,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_import(
|
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||||
self, import_config: dict[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Import config from configuration.yaml."""
|
"""Import config from configuration.yaml."""
|
||||||
if self._async_current_entries():
|
if self._async_current_entries():
|
||||||
return self.async_abort(reason="single_instance_allowed")
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
if CONF_SEND_INTERVAL in import_config:
|
if CONF_SEND_INTERVAL in import_data:
|
||||||
self._options[CONF_SEND_INTERVAL] = import_config.pop(CONF_SEND_INTERVAL)
|
self._options[CONF_SEND_INTERVAL] = import_data.pop(CONF_SEND_INTERVAL)
|
||||||
if CONF_MAX_DELAY in import_config:
|
if CONF_MAX_DELAY in import_data:
|
||||||
self._options[CONF_MAX_DELAY] = import_config.pop(CONF_MAX_DELAY)
|
self._options[CONF_MAX_DELAY] = import_data.pop(CONF_MAX_DELAY)
|
||||||
self._data = import_config
|
self._data = import_data
|
||||||
errors = await validate_data(self._data)
|
errors = await validate_data(self._data)
|
||||||
if errors:
|
if errors:
|
||||||
return self.async_abort(reason=errors["base"])
|
return self.async_abort(reason=errors["base"])
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"create": "mdi:cloud-upload"
|
"create": {
|
||||||
|
"service": "mdi:cloud-upload"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,6 +17,7 @@ from homeassistant.const import CONF_HOST, CONF_MODEL, Platform
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
import homeassistant.helpers.device_registry as dr
|
import homeassistant.helpers.device_registry as dr
|
||||||
|
from homeassistant.util.ssl import get_default_context
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .websocket import BangOlufsenWebsocket
|
from .websocket import BangOlufsenWebsocket
|
||||||
@@ -48,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
model=entry.data[CONF_MODEL],
|
model=entry.data[CONF_MODEL],
|
||||||
)
|
)
|
||||||
|
|
||||||
client = MozartClient(host=entry.data[CONF_HOST])
|
client = MozartClient(host=entry.data[CONF_HOST], ssl_context=get_default_context())
|
||||||
|
|
||||||
# Check API and WebSocket connection
|
# Check API and WebSocket connection
|
||||||
try:
|
try:
|
||||||
|
@@ -14,6 +14,7 @@ from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
|||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_HOST, CONF_MODEL
|
from homeassistant.const import CONF_HOST, CONF_MODEL
|
||||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||||
|
from homeassistant.util.ssl import get_default_context
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_FRIENDLY_NAME,
|
ATTR_FRIENDLY_NAME,
|
||||||
@@ -87,7 +88,9 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
errors={"base": _exception_map[type(error)]},
|
errors={"base": _exception_map[type(error)]},
|
||||||
)
|
)
|
||||||
|
|
||||||
self._client = MozartClient(self._host)
|
self._client = MozartClient(
|
||||||
|
host=self._host, ssl_context=get_default_context()
|
||||||
|
)
|
||||||
|
|
||||||
# Try to get information from Beolink self method.
|
# Try to get information from Beolink self method.
|
||||||
async with self._client:
|
async with self._client:
|
||||||
@@ -136,7 +139,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_abort(reason="ipv6_address")
|
return self.async_abort(reason="ipv6_address")
|
||||||
|
|
||||||
# Check connection to ensure valid address is received
|
# Check connection to ensure valid address is received
|
||||||
self._client = MozartClient(self._host)
|
self._client = MozartClient(self._host, ssl_context=get_default_context())
|
||||||
|
|
||||||
async with self._client:
|
async with self._client:
|
||||||
try:
|
try:
|
||||||
|
@@ -6,6 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
|
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["mozart-api==3.4.1.8.6"],
|
"requirements": ["mozart-api==3.4.1.8.8"],
|
||||||
"zeroconf": ["_bangolufsen._tcp.local."]
|
"zeroconf": ["_bangolufsen._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"reload": "mdi:reload"
|
"reload": {
|
||||||
|
"service": "mdi:reload"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"set_all_zones": "mdi:home-sound-in"
|
"set_all_zones": {
|
||||||
|
"service": "mdi:home-sound-in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,15 +35,11 @@ from .const import (
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def host_port(data):
|
|
||||||
"""Return a list with host and port."""
|
|
||||||
return (data[CONF_HOST], data[CONF_PORT])
|
|
||||||
|
|
||||||
|
|
||||||
def create_schema(previous_input=None):
|
def create_schema(previous_input=None):
|
||||||
"""Create a schema with given values as default."""
|
"""Create a schema with given values as default."""
|
||||||
if previous_input is not None:
|
if previous_input is not None:
|
||||||
host, port = host_port(previous_input)
|
host = previous_input[CONF_HOST]
|
||||||
|
port = previous_input[CONF_PORT]
|
||||||
else:
|
else:
|
||||||
host = DEFAULT_HOST
|
host = DEFAULT_HOST
|
||||||
port = DEFAULT_PORT
|
port = DEFAULT_PORT
|
||||||
@@ -70,9 +66,9 @@ class BleBoxConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
"""Initialize the BleBox config flow."""
|
"""Initialize the BleBox config flow."""
|
||||||
self.device_config = {}
|
self.device_config: dict[str, Any] = {}
|
||||||
|
|
||||||
def handle_step_exception(
|
def handle_step_exception(
|
||||||
self, step, exception, schema, host, port, message_id, log_fn
|
self, step, exception, schema, host, port, message_id, log_fn
|
||||||
@@ -146,7 +142,9 @@ class BleBoxConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
"""Handle initial user-triggered config step."""
|
"""Handle initial user-triggered config step."""
|
||||||
hass = self.hass
|
hass = self.hass
|
||||||
schema = create_schema(user_input)
|
schema = create_schema(user_input)
|
||||||
@@ -159,14 +157,14 @@ class BleBoxConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
description_placeholders={},
|
description_placeholders={},
|
||||||
)
|
)
|
||||||
|
|
||||||
addr = host_port(user_input)
|
host = user_input[CONF_HOST]
|
||||||
|
port = user_input[CONF_PORT]
|
||||||
|
|
||||||
username = user_input.get(CONF_USERNAME)
|
username = user_input.get(CONF_USERNAME)
|
||||||
password = user_input.get(CONF_PASSWORD)
|
password = user_input.get(CONF_PASSWORD)
|
||||||
|
|
||||||
for entry in self._async_current_entries():
|
for entry in self._async_current_entries():
|
||||||
if addr == host_port(entry.data):
|
if host == entry.data[CONF_HOST] and port == entry.data[CONF_PORT]:
|
||||||
host, port = addr
|
|
||||||
return self.async_abort(
|
return self.async_abort(
|
||||||
reason=ADDRESS_ALREADY_CONFIGURED,
|
reason=ADDRESS_ALREADY_CONFIGURED,
|
||||||
description_placeholders={"address": f"{host}:{port}"},
|
description_placeholders={"address": f"{host}:{port}"},
|
||||||
@@ -174,27 +172,35 @@ class BleBoxConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
websession = get_maybe_authenticated_session(hass, password, username)
|
websession = get_maybe_authenticated_session(hass, password, username)
|
||||||
|
|
||||||
api_host = ApiHost(*addr, DEFAULT_SETUP_TIMEOUT, websession, hass.loop, _LOGGER)
|
api_host = ApiHost(
|
||||||
|
host, port, DEFAULT_SETUP_TIMEOUT, websession, hass.loop, _LOGGER
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
product = await Box.async_from_host(api_host)
|
product = await Box.async_from_host(api_host)
|
||||||
|
|
||||||
except UnsupportedBoxVersion as ex:
|
except UnsupportedBoxVersion as ex:
|
||||||
return self.handle_step_exception(
|
return self.handle_step_exception(
|
||||||
"user", ex, schema, *addr, UNSUPPORTED_VERSION, _LOGGER.debug
|
"user",
|
||||||
|
ex,
|
||||||
|
schema,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
UNSUPPORTED_VERSION,
|
||||||
|
_LOGGER.debug,
|
||||||
)
|
)
|
||||||
except UnauthorizedRequest as ex:
|
except UnauthorizedRequest as ex:
|
||||||
return self.handle_step_exception(
|
return self.handle_step_exception(
|
||||||
"user", ex, schema, *addr, CANNOT_CONNECT, _LOGGER.error
|
"user", ex, schema, host, port, CANNOT_CONNECT, _LOGGER.error
|
||||||
)
|
)
|
||||||
|
|
||||||
except Error as ex:
|
except Error as ex:
|
||||||
return self.handle_step_exception(
|
return self.handle_step_exception(
|
||||||
"user", ex, schema, *addr, CANNOT_CONNECT, _LOGGER.warning
|
"user", ex, schema, host, port, CANNOT_CONNECT, _LOGGER.warning
|
||||||
)
|
)
|
||||||
|
|
||||||
except RuntimeError as ex:
|
except RuntimeError as ex:
|
||||||
return self.handle_step_exception(
|
return self.handle_step_exception(
|
||||||
"user", ex, schema, *addr, UNKNOWN, _LOGGER.error
|
"user", ex, schema, host, port, UNKNOWN, _LOGGER.error
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if configured but IP changed since
|
# Check if configured but IP changed since
|
||||||
|
@@ -60,6 +60,9 @@ COLOR_MODE_MAP = {
|
|||||||
class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
||||||
"""Representation of BleBox lights."""
|
"""Representation of BleBox lights."""
|
||||||
|
|
||||||
|
_attr_max_mireds = 370 # 1,000,000 divided by 2700 Kelvin = 370 Mireds
|
||||||
|
_attr_min_mireds = 154 # 1,000,000 divided by 6500 Kelvin = 154 Mireds
|
||||||
|
|
||||||
def __init__(self, feature: blebox_uniapi.light.Light) -> None:
|
def __init__(self, feature: blebox_uniapi.light.Light) -> None:
|
||||||
"""Initialize a BleBox light."""
|
"""Initialize a BleBox light."""
|
||||||
super().__init__(feature)
|
super().__init__(feature)
|
||||||
@@ -87,12 +90,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
|||||||
|
|
||||||
Set values to _attr_ibutes if needed.
|
Set values to _attr_ibutes if needed.
|
||||||
"""
|
"""
|
||||||
color_mode_tmp = COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF)
|
return COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF)
|
||||||
if color_mode_tmp == ColorMode.COLOR_TEMP:
|
|
||||||
self._attr_min_mireds = 1
|
|
||||||
self._attr_max_mireds = 255
|
|
||||||
|
|
||||||
return color_mode_tmp
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_color_modes(self):
|
def supported_color_modes(self):
|
||||||
|
@@ -12,10 +12,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"record": "mdi:video-box",
|
"record": {
|
||||||
"trigger_camera": "mdi:image-refresh",
|
"service": "mdi:video-box"
|
||||||
"save_video": "mdi:file-video",
|
},
|
||||||
"save_recent_clips": "mdi:file-video",
|
"trigger_camera": {
|
||||||
"send_pin": "mdi:two-factor-authentication"
|
"service": "mdi:image-refresh"
|
||||||
|
},
|
||||||
|
"save_video": {
|
||||||
|
"service": "mdi:file-video"
|
||||||
|
},
|
||||||
|
"save_recent_clips": {
|
||||||
|
"service": "mdi:file-video"
|
||||||
|
},
|
||||||
|
"send_pin": {
|
||||||
|
"service": "mdi:two-factor-authentication"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,16 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"join": "mdi:link-variant",
|
"join": {
|
||||||
"unjoin": "mdi:link-variant-off",
|
"service": "mdi:link-variant"
|
||||||
"set_sleep_timer": "mdi:sleep",
|
},
|
||||||
"clear_sleep_timer": "mdi:sleep-off"
|
"unjoin": {
|
||||||
|
"service": "mdi:link-variant-off"
|
||||||
|
},
|
||||||
|
"set_sleep_timer": {
|
||||||
|
"service": "mdi:sleep"
|
||||||
|
},
|
||||||
|
"clear_sleep_timer": {
|
||||||
|
"service": "mdi:sleep-off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -309,7 +309,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _start_poll_command(self):
|
async def _poll_loop(self):
|
||||||
"""Loop which polls the status of the player."""
|
"""Loop which polls the status of the player."""
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -335,7 +335,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
self._polling_task = self.hass.async_create_background_task(
|
self._polling_task = self.hass.async_create_background_task(
|
||||||
self._start_poll_command(),
|
self._poll_loop(),
|
||||||
name=f"bluesound.polling_{self.host}:{self.port}",
|
name=f"bluesound.polling_{self.host}:{self.port}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -345,7 +345,9 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
|
|
||||||
assert self._polling_task is not None
|
assert self._polling_task is not None
|
||||||
if self._polling_task.cancel():
|
if self._polling_task.cancel():
|
||||||
await self._polling_task
|
# the sleeps in _poll_loop will raise CancelledError
|
||||||
|
with suppress(CancelledError):
|
||||||
|
await self._polling_task
|
||||||
|
|
||||||
self.hass.data[DATA_BLUESOUND].remove(self)
|
self.hass.data[DATA_BLUESOUND].remove(self)
|
||||||
|
|
||||||
|
@@ -18,8 +18,8 @@
|
|||||||
"bleak-retry-connector==3.5.0",
|
"bleak-retry-connector==3.5.0",
|
||||||
"bluetooth-adapters==0.19.4",
|
"bluetooth-adapters==0.19.4",
|
||||||
"bluetooth-auto-recovery==1.4.2",
|
"bluetooth-auto-recovery==1.4.2",
|
||||||
"bluetooth-data-tools==1.19.4",
|
"bluetooth-data-tools==1.20.0",
|
||||||
"dbus-fast==2.23.0",
|
"dbus-fast==2.24.0",
|
||||||
"habluetooth==3.3.2"
|
"habluetooth==3.4.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"update": "mdi:update"
|
"update": {
|
||||||
|
"service": "mdi:update"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
from homeassistant.util.ssl import get_default_context
|
||||||
|
|
||||||
from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS
|
from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||||||
entry.data[CONF_PASSWORD],
|
entry.data[CONF_PASSWORD],
|
||||||
get_region_from_name(entry.data[CONF_REGION]),
|
get_region_from_name(entry.data[CONF_REGION]),
|
||||||
observer_position=GPSPosition(hass.config.latitude, hass.config.longitude),
|
observer_position=GPSPosition(hass.config.latitude, hass.config.longitude),
|
||||||
|
verify=get_default_context(),
|
||||||
)
|
)
|
||||||
self.read_only = entry.options[CONF_READ_ONLY]
|
self.read_only = entry.options[CONF_READ_ONLY]
|
||||||
self._entry = entry
|
self._entry = entry
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["bimmer_connected"],
|
"loggers": ["bimmer_connected"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["bimmer-connected[china]==0.16.1"]
|
"requirements": ["bimmer-connected[china]==0.16.3"]
|
||||||
}
|
}
|
||||||
|
@@ -148,7 +148,8 @@
|
|||||||
"cooling": "Cooling",
|
"cooling": "Cooling",
|
||||||
"heating": "Heating",
|
"heating": "Heating",
|
||||||
"inactive": "Inactive",
|
"inactive": "Inactive",
|
||||||
"standby": "Standby"
|
"standby": "Standby",
|
||||||
|
"ventilation": "Ventilation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"front_left_current_pressure": {
|
"front_left_current_pressure": {
|
||||||
|
@@ -96,12 +96,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"set_fan_speed_tracked_state": "mdi:fan",
|
"set_fan_speed_tracked_state": {
|
||||||
"set_switch_power_tracked_state": "mdi:toggle-switch-variant",
|
"service": "mdi:fan"
|
||||||
"set_light_power_tracked_state": "mdi:lightbulb",
|
},
|
||||||
"set_light_brightness_tracked_state": "mdi:lightbulb-on",
|
"set_switch_power_tracked_state": {
|
||||||
"start_increasing_brightness": "mdi:brightness-7",
|
"service": "mdi:toggle-switch-variant"
|
||||||
"start_decreasing_brightness": "mdi:brightness-1",
|
},
|
||||||
"stop": "mdi:stop"
|
"set_light_power_tracked_state": {
|
||||||
|
"service": "mdi:lightbulb"
|
||||||
|
},
|
||||||
|
"set_light_brightness_tracked_state": {
|
||||||
|
"service": "mdi:lightbulb-on"
|
||||||
|
},
|
||||||
|
"start_increasing_brightness": {
|
||||||
|
"service": "mdi:brightness-7"
|
||||||
|
},
|
||||||
|
"start_decreasing_brightness": {
|
||||||
|
"service": "mdi:brightness-1"
|
||||||
|
},
|
||||||
|
"stop": {
|
||||||
|
"service": "mdi:stop"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"send_message": "mdi:cellphone-message"
|
"send_message": {
|
||||||
|
"service": "mdi:cellphone-message"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@ import errno
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import broadlink as blk
|
import broadlink as blk
|
||||||
from broadlink.exceptions import (
|
from broadlink.exceptions import (
|
||||||
@@ -39,9 +39,11 @@ class BroadlinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize the Broadlink flow."""
|
"""Initialize the Broadlink flow."""
|
||||||
self.device = None
|
self.device: blk.Device | None = None
|
||||||
|
|
||||||
async def async_set_device(self, device, raise_on_progress=True):
|
async def async_set_device(
|
||||||
|
self, device: blk.Device, raise_on_progress: bool = True
|
||||||
|
) -> None:
|
||||||
"""Define a device for the config flow."""
|
"""Define a device for the config flow."""
|
||||||
if device.type not in DEVICE_TYPES:
|
if device.type not in DEVICE_TYPES:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
@@ -90,7 +92,9 @@ class BroadlinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
await self.async_set_device(device)
|
await self.async_set_device(device)
|
||||||
return await self.async_step_auth()
|
return await self.async_step_auth()
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
"""Handle a flow initiated by the user."""
|
"""Handle a flow initiated by the user."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
@@ -127,6 +131,8 @@ class BroadlinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
return await self.async_step_auth()
|
return await self.async_step_auth()
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert self.device
|
||||||
if device.mac == self.device.mac:
|
if device.mac == self.device.mac:
|
||||||
await self.async_set_device(device, raise_on_progress=False)
|
await self.async_set_device(device, raise_on_progress=False)
|
||||||
return await self.async_step_auth()
|
return await self.async_step_auth()
|
||||||
@@ -308,10 +314,10 @@ class BroadlinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
step_id="finish", data_schema=vol.Schema(data_schema), errors=errors
|
step_id="finish", data_schema=vol.Schema(data_schema), errors=errors
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_import(self, import_info):
|
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||||
"""Import a device."""
|
"""Import a device."""
|
||||||
self._async_abort_entries_match({CONF_HOST: import_info[CONF_HOST]})
|
self._async_abort_entries_match({CONF_HOST: import_data[CONF_HOST]})
|
||||||
return await self.async_step_user(import_info)
|
return await self.async_step_user(import_data)
|
||||||
|
|
||||||
async def async_step_reauth(
|
async def async_step_reauth(
|
||||||
self, entry_data: Mapping[str, Any]
|
self, entry_data: Mapping[str, Any]
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"browse_url": "mdi:web"
|
"browse_url": {
|
||||||
|
"service": "mdi:web"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,5 +20,8 @@ async def async_get_config_entry_diagnostics(
|
|||||||
return {
|
return {
|
||||||
"info": data.info.to_dict(),
|
"info": data.info.to_dict(),
|
||||||
"device": data.device.to_dict(),
|
"device": data.device.to_dict(),
|
||||||
"state": data.coordinator.data.state.to_dict(),
|
"coordinator_data": {
|
||||||
|
"state": data.coordinator.data.state.to_dict(),
|
||||||
|
},
|
||||||
|
"static": data.static.to_dict(),
|
||||||
}
|
}
|
||||||
|
@@ -22,10 +22,10 @@ class BSBLanEntity(CoordinatorEntity[BSBLanUpdateCoordinator]):
|
|||||||
def __init__(self, coordinator: BSBLanUpdateCoordinator, data: BSBLanData) -> None:
|
def __init__(self, coordinator: BSBLanUpdateCoordinator, data: BSBLanData) -> None:
|
||||||
"""Initialize BSBLan entity."""
|
"""Initialize BSBLan entity."""
|
||||||
super().__init__(coordinator, data)
|
super().__init__(coordinator, data)
|
||||||
host = self.coordinator.config_entry.data["host"]
|
host = coordinator.config_entry.data["host"]
|
||||||
mac = self.coordinator.config_entry.data["mac"]
|
mac = data.device.MAC
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, data.device.MAC)},
|
identifiers={(DOMAIN, mac)},
|
||||||
connections={(CONNECTION_NETWORK_MAC, format_mac(mac))},
|
connections={(CONNECTION_NETWORK_MAC, format_mac(mac))},
|
||||||
name=data.device.name,
|
name=data.device.name,
|
||||||
manufacturer="BSBLAN Inc.",
|
manufacturer="BSBLAN Inc.",
|
||||||
|
@@ -7,6 +7,9 @@ from typing import Any
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
||||||
|
from homeassistant.components.device_automation.exceptions import (
|
||||||
|
InvalidDeviceAutomationConfig,
|
||||||
|
)
|
||||||
from homeassistant.components.homeassistant.triggers import event as event_trigger
|
from homeassistant.components.homeassistant.triggers import event as event_trigger
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_DEVICE_ID,
|
CONF_DEVICE_ID,
|
||||||
@@ -43,33 +46,46 @@ TRIGGERS_BY_EVENT_CLASS = {
|
|||||||
EVENT_CLASS_DIMMER: {"rotate_left", "rotate_right"},
|
EVENT_CLASS_DIMMER: {"rotate_left", "rotate_right"},
|
||||||
}
|
}
|
||||||
|
|
||||||
SCHEMA_BY_EVENT_CLASS = {
|
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||||
EVENT_CLASS_BUTTON: DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
{vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str}
|
||||||
{
|
)
|
||||||
vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_BUTTON]),
|
|
||||||
vol.Required(CONF_SUBTYPE): vol.In(
|
|
||||||
TRIGGERS_BY_EVENT_CLASS[EVENT_CLASS_BUTTON]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
EVENT_CLASS_DIMMER: DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_DIMMER]),
|
|
||||||
vol.Required(CONF_SUBTYPE): vol.In(
|
|
||||||
TRIGGERS_BY_EVENT_CLASS[EVENT_CLASS_DIMMER]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_validate_trigger_config(
|
async def async_validate_trigger_config(
|
||||||
hass: HomeAssistant, config: ConfigType
|
hass: HomeAssistant, config: ConfigType
|
||||||
) -> ConfigType:
|
) -> ConfigType:
|
||||||
"""Validate trigger config."""
|
"""Validate trigger config."""
|
||||||
return SCHEMA_BY_EVENT_CLASS.get(config[CONF_TYPE], DEVICE_TRIGGER_BASE_SCHEMA)( # type: ignore[no-any-return]
|
config = TRIGGER_SCHEMA(config)
|
||||||
config
|
event_class = config[CONF_TYPE]
|
||||||
|
event_type = config[CONF_SUBTYPE]
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
device = device_registry.async_get(config[CONF_DEVICE_ID])
|
||||||
|
assert device is not None
|
||||||
|
config_entries = [
|
||||||
|
hass.config_entries.async_get_entry(entry_id)
|
||||||
|
for entry_id in device.config_entries
|
||||||
|
]
|
||||||
|
bthome_config_entry = next(
|
||||||
|
iter(entry for entry in config_entries if entry and entry.domain == DOMAIN)
|
||||||
)
|
)
|
||||||
|
event_classes: list[str] = bthome_config_entry.data.get(
|
||||||
|
CONF_DISCOVERED_EVENT_CLASSES, []
|
||||||
|
)
|
||||||
|
|
||||||
|
if event_class not in event_classes:
|
||||||
|
raise InvalidDeviceAutomationConfig(
|
||||||
|
f"BTHome trigger {event_class} is not valid for device "
|
||||||
|
f"{device} ({config[CONF_DEVICE_ID]})"
|
||||||
|
)
|
||||||
|
|
||||||
|
if event_type not in TRIGGERS_BY_EVENT_CLASS.get(event_class.split("_")[0], ()):
|
||||||
|
raise InvalidDeviceAutomationConfig(
|
||||||
|
f"BTHome trigger {event_type} is not valid for device "
|
||||||
|
f"{device} ({config[CONF_DEVICE_ID]})"
|
||||||
|
)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
async def async_get_triggers(
|
async def async_get_triggers(
|
||||||
|
@@ -27,6 +27,7 @@ from homeassistant.const import (
|
|||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
|
UnitOfConductivity,
|
||||||
UnitOfElectricCurrent,
|
UnitOfElectricCurrent,
|
||||||
UnitOfElectricPotential,
|
UnitOfElectricPotential,
|
||||||
UnitOfEnergy,
|
UnitOfEnergy,
|
||||||
@@ -356,6 +357,16 @@ SENSOR_DESCRIPTIONS = {
|
|||||||
native_unit_of_measurement=UnitOfVolume.LITERS,
|
native_unit_of_measurement=UnitOfVolume.LITERS,
|
||||||
state_class=SensorStateClass.TOTAL,
|
state_class=SensorStateClass.TOTAL,
|
||||||
),
|
),
|
||||||
|
# Conductivity (µS/cm)
|
||||||
|
(
|
||||||
|
BTHomeSensorDeviceClass.CONDUCTIVITY,
|
||||||
|
Units.CONDUCTIVITY,
|
||||||
|
): SensorEntityDescription(
|
||||||
|
key=f"{BTHomeSensorDeviceClass.CONDUCTIVITY}_{Units.CONDUCTIVITY}",
|
||||||
|
device_class=SensorDeviceClass.CONDUCTIVITY,
|
||||||
|
native_unit_of_measurement=UnitOfConductivity.MICROSIEMENS,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
DOMAIN = "buienradar"
|
DOMAIN = "buienradar"
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT = 60
|
||||||
DEFAULT_TIMEFRAME = 60
|
DEFAULT_TIMEFRAME = 60
|
||||||
|
|
||||||
DEFAULT_DIMENSION = 700
|
DEFAULT_DIMENSION = 700
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
"""Shared utilities for different supported platforms."""
|
"""Shared utilities for different supported platforms."""
|
||||||
|
|
||||||
from asyncio import timeout
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from buienradar.buienradar import parse_data
|
from buienradar.buienradar import parse_data
|
||||||
@@ -27,12 +27,12 @@ from buienradar.constants import (
|
|||||||
from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url
|
from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url
|
||||||
|
|
||||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||||
from homeassistant.core import CALLBACK_TYPE, callback
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import SCHEDULE_NOK, SCHEDULE_OK
|
from .const import DEFAULT_TIMEOUT, SCHEDULE_NOK, SCHEDULE_OK
|
||||||
|
|
||||||
__all__ = ["BrData"]
|
__all__ = ["BrData"]
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -59,10 +59,10 @@ class BrData:
|
|||||||
load_error_count: int = WARN_THRESHOLD
|
load_error_count: int = WARN_THRESHOLD
|
||||||
rain_error_count: int = WARN_THRESHOLD
|
rain_error_count: int = WARN_THRESHOLD
|
||||||
|
|
||||||
def __init__(self, hass, coordinates, timeframe, devices):
|
def __init__(self, hass: HomeAssistant, coordinates, timeframe, devices) -> None:
|
||||||
"""Initialize the data object."""
|
"""Initialize the data object."""
|
||||||
self.devices = devices
|
self.devices = devices
|
||||||
self.data = {}
|
self.data: dict[str, Any] | None = {}
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.coordinates = coordinates
|
self.coordinates = coordinates
|
||||||
self.timeframe = timeframe
|
self.timeframe = timeframe
|
||||||
@@ -93,9 +93,9 @@ class BrData:
|
|||||||
resp = None
|
resp = None
|
||||||
try:
|
try:
|
||||||
websession = async_get_clientsession(self.hass)
|
websession = async_get_clientsession(self.hass)
|
||||||
async with timeout(10):
|
async with websession.get(
|
||||||
resp = await websession.get(url)
|
url, timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT)
|
||||||
|
) as resp:
|
||||||
result[STATUS_CODE] = resp.status
|
result[STATUS_CODE] = resp.status
|
||||||
result[CONTENT] = await resp.text()
|
result[CONTENT] = await resp.text()
|
||||||
if resp.status == HTTPStatus.OK:
|
if resp.status == HTTPStatus.OK:
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user