mirror of
https://github.com/home-assistant/core.git
synced 2026-03-12 05:51:59 +01:00
Compare commits
364 Commits
config-yam
...
state_attr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9537f7357 | ||
|
|
2eb65ab314 | ||
|
|
402a37b435 | ||
|
|
aa66e8ef0c | ||
|
|
f1a1e284b7 | ||
|
|
08594f4e0c | ||
|
|
8d810588f8 | ||
|
|
70faad15d5 | ||
|
|
d447843687 | ||
|
|
83b64e29fa | ||
|
|
4558a10e05 | ||
|
|
5ad9e81082 | ||
|
|
ba00a14772 | ||
|
|
49f4d07eeb | ||
|
|
5d271a0d30 | ||
|
|
474b683d3c | ||
|
|
d37106a360 | ||
|
|
57a33dd34d | ||
|
|
e115c90719 | ||
|
|
6ad3adf0c3 | ||
|
|
2a8d59be4c | ||
|
|
6e6e35bc3b | ||
|
|
795b4c8414 | ||
|
|
16389dc18e | ||
|
|
e7a1c8d001 | ||
|
|
4efb10dae1 | ||
|
|
f163576e78 | ||
|
|
cad8f97e97 | ||
|
|
4ae6099d84 | ||
|
|
60dc88fa15 | ||
|
|
2d2c6d676d | ||
|
|
f3879335ab | ||
|
|
11bc00038e | ||
|
|
6845e8b880 | ||
|
|
5741016931 | ||
|
|
6cbc4e7f62 | ||
|
|
4064df0114 | ||
|
|
789f850691 | ||
|
|
efca71852b | ||
|
|
1967e9f309 | ||
|
|
6ac0c163aa | ||
|
|
bbe20fd698 | ||
|
|
f576743340 | ||
|
|
3b4a1fba5f | ||
|
|
1677a9bfa6 | ||
|
|
0d9c458705 | ||
|
|
57026a862d | ||
|
|
fd05be4c52 | ||
|
|
b1f038849e | ||
|
|
b46c9ccc65 | ||
|
|
80601426cf | ||
|
|
9519bd2428 | ||
|
|
be0b7f06a8 | ||
|
|
d30c6de168 | ||
|
|
0fa666518e | ||
|
|
cf454a1fa3 | ||
|
|
a36733c4dc | ||
|
|
bf846e0756 | ||
|
|
c037dad093 | ||
|
|
ce11e66e1f | ||
|
|
f38ca7b04a | ||
|
|
01200ef0a8 | ||
|
|
c5e0c78cbc | ||
|
|
7681caa936 | ||
|
|
230a2ff045 | ||
|
|
9d828502a3 | ||
|
|
28088a7e1a | ||
|
|
9e8171fb77 | ||
|
|
1660d3b28a | ||
|
|
2ef81a54a5 | ||
|
|
ce6154839e | ||
|
|
a25300b8e1 | ||
|
|
6fa8e71b21 | ||
|
|
c983978a10 | ||
|
|
68b8b6b675 | ||
|
|
ee4d313b10 | ||
|
|
5e665093c9 | ||
|
|
9a5f509ab9 | ||
|
|
8d0cd5edaa | ||
|
|
71726272f5 | ||
|
|
9c6c27ab56 | ||
|
|
db20cf8161 | ||
|
|
59b6270157 | ||
|
|
a65ba01bbe | ||
|
|
a5d0350560 | ||
|
|
368993556f | ||
|
|
23ea17eaef | ||
|
|
6ace93e45b | ||
|
|
237a0ae03f | ||
|
|
6067be6f49 | ||
|
|
a35c3d5de5 | ||
|
|
e9c3634cb6 | ||
|
|
2ba4544180 | ||
|
|
5235ce7ae4 | ||
|
|
56b601e577 | ||
|
|
f01a0586cb | ||
|
|
ca641a097b | ||
|
|
df2f9d9ef8 | ||
|
|
501301f4e0 | ||
|
|
89231a1a29 | ||
|
|
fe11a6d38f | ||
|
|
3154c3c962 | ||
|
|
5031323dea | ||
|
|
017a9e6938 | ||
|
|
9e974ab30e | ||
|
|
30c0d6792a | ||
|
|
9ffb9aa824 | ||
|
|
9ad71711da | ||
|
|
ef83165159 | ||
|
|
f0108c1175 | ||
|
|
802aa991a9 | ||
|
|
f055c6c7fd | ||
|
|
2a8b045f43 | ||
|
|
281f439bc9 | ||
|
|
71b420b433 | ||
|
|
2f02d0f0dc | ||
|
|
37cb3cbd50 | ||
|
|
beec21c4a9 | ||
|
|
642f603ea2 | ||
|
|
a3d8d76678 | ||
|
|
c25feaa62b | ||
|
|
50bde6fccd | ||
|
|
1b7398c271 | ||
|
|
7e4b8e802e | ||
|
|
4bcea27151 | ||
|
|
ffca43027f | ||
|
|
01e94ca5b2 | ||
|
|
b8ea6b4162 | ||
|
|
1471cb93bc | ||
|
|
2f7ac2b439 | ||
|
|
0accb403be | ||
|
|
f49a323faf | ||
|
|
21d303dbbc | ||
|
|
c080a460a2 | ||
|
|
75d675f299 | ||
|
|
a7e7d01b7a | ||
|
|
8a0569e279 | ||
|
|
e8279bd20f | ||
|
|
852dbf8986 | ||
|
|
6f0eb1d07a | ||
|
|
6f68d91593 | ||
|
|
ffc17b6e91 | ||
|
|
0d04d79844 | ||
|
|
f57884cb95 | ||
|
|
3a83fe5c72 | ||
|
|
973feb71c1 | ||
|
|
ecee23fc7a | ||
|
|
442d2282dc | ||
|
|
8853d3e17d | ||
|
|
6d1e387911 | ||
|
|
13fe135e7f | ||
|
|
618687ea05 | ||
|
|
8b545a6e76 | ||
|
|
42fa13200d | ||
|
|
d56e944a86 | ||
|
|
fb357390ce | ||
|
|
702450e209 | ||
|
|
bbe45e0759 | ||
|
|
92902c7aa1 | ||
|
|
5d92dd7760 | ||
|
|
0ab62dabde | ||
|
|
fc68828c78 | ||
|
|
7644036592 | ||
|
|
f19068f7de | ||
|
|
13d2211755 | ||
|
|
87e63591d1 | ||
|
|
fc02bbcdd0 | ||
|
|
388d619604 | ||
|
|
3777acff95 | ||
|
|
e0fd6784cf | ||
|
|
305463d882 | ||
|
|
de16edc55b | ||
|
|
bd6438937b | ||
|
|
45e453791e | ||
|
|
152137a3a2 | ||
|
|
e059c51b1d | ||
|
|
9ef66a3a90 | ||
|
|
494f8c32d5 | ||
|
|
51f90a328b | ||
|
|
b7bdb7b32a | ||
|
|
76c8bae098 | ||
|
|
59a75e74fe | ||
|
|
a4af1ce5f8 | ||
|
|
30ea0b4923 | ||
|
|
fb889dd524 | ||
|
|
31055c5cde | ||
|
|
a264e5949f | ||
|
|
84260ac3f7 | ||
|
|
f50a35877d | ||
|
|
6bc94a318a | ||
|
|
b0904917ca | ||
|
|
536cfc4c67 | ||
|
|
27b647fa36 | ||
|
|
16fb2dfa91 | ||
|
|
664b75e060 | ||
|
|
1cd302eb17 | ||
|
|
8da86796d2 | ||
|
|
33c0edc994 | ||
|
|
3e8833da54 | ||
|
|
3858d557b3 | ||
|
|
0923bed4b6 | ||
|
|
9b8432eac3 | ||
|
|
5232c05702 | ||
|
|
e5f77801a7 | ||
|
|
bc138b3485 | ||
|
|
ae90c5fa92 | ||
|
|
2fce45abe1 | ||
|
|
e4417f7b00 | ||
|
|
b57c7f8a95 | ||
|
|
0618460d73 | ||
|
|
92dd045772 | ||
|
|
fc723e1a42 | ||
|
|
5907356309 | ||
|
|
1c221b4714 | ||
|
|
05d57167d2 | ||
|
|
69a98dd53e | ||
|
|
3c7dd93c7f | ||
|
|
1327712be4 | ||
|
|
933e57ba6a | ||
|
|
77d54aadc6 | ||
|
|
5fe2ab93ff | ||
|
|
0e4698eb99 | ||
|
|
698c5eca00 | ||
|
|
c7776057b7 | ||
|
|
e87c677cc4 | ||
|
|
c3858a0841 | ||
|
|
42bc5c3a5f | ||
|
|
76bc58da2c | ||
|
|
fc8719ce35 | ||
|
|
60a4a97d9c | ||
|
|
284721e1df | ||
|
|
bfa707d79e | ||
|
|
633e2e7469 | ||
|
|
ad1c6846e7 | ||
|
|
f75140b626 | ||
|
|
f83757da7c | ||
|
|
ca338c98f3 | ||
|
|
18a8afb017 | ||
|
|
0136e9c7eb | ||
|
|
d88c736016 | ||
|
|
780dc178a1 | ||
|
|
b7ba945dfc | ||
|
|
01de7052af | ||
|
|
3fe6a31ee9 | ||
|
|
95570643ec | ||
|
|
e3210b0ab9 | ||
|
|
2edabf903a | ||
|
|
0e4e703b64 | ||
|
|
88624f5179 | ||
|
|
4a5fdfc0ec | ||
|
|
c6e91afae4 | ||
|
|
db5e7e4521 | ||
|
|
25489c224b | ||
|
|
c4f64598a0 | ||
|
|
59e579cf5a | ||
|
|
831c28cf2c | ||
|
|
be1affc6ba | ||
|
|
94a25b5688 | ||
|
|
382940d661 | ||
|
|
b8e1c0cf2c | ||
|
|
0d23d8dc09 | ||
|
|
b750de1e3e | ||
|
|
7d7e8e0bde | ||
|
|
d6f355355f | ||
|
|
5dad64e54c | ||
|
|
c311ff0464 | ||
|
|
c45675a01f | ||
|
|
9d92141812 | ||
|
|
501b973a98 | ||
|
|
fd4d8137da | ||
|
|
33881c1912 | ||
|
|
9bdb03dbe8 | ||
|
|
d2178ba458 | ||
|
|
06cdf3c5d2 | ||
|
|
84c994ab80 | ||
|
|
1d5913d7a5 | ||
|
|
05acba37c7 | ||
|
|
7496406156 | ||
|
|
543f2b1396 | ||
|
|
3df2bbda80 | ||
|
|
b661d37a86 | ||
|
|
2102babc6d | ||
|
|
f3a1cab582 | ||
|
|
03c9ce25c8 | ||
|
|
8fcabcec16 | ||
|
|
2a33096074 | ||
|
|
14a9eada09 | ||
|
|
4a00f78e90 | ||
|
|
abef46864e | ||
|
|
73b28f1ee2 | ||
|
|
7379d41393 | ||
|
|
89acb02519 | ||
|
|
e343e90da2 | ||
|
|
e9a576494b | ||
|
|
4e047b56d8 | ||
|
|
a1e95c483d | ||
|
|
9cb6e02c5f | ||
|
|
2c75e3289a | ||
|
|
348012a6b8 | ||
|
|
e0db00e089 | ||
|
|
b2280198d9 | ||
|
|
9cc4a3e427 | ||
|
|
f94a075641 | ||
|
|
f1856e6ef6 | ||
|
|
ed35bafa6c | ||
|
|
66e16d728b | ||
|
|
a806efa7e2 | ||
|
|
ad4b4bd221 | ||
|
|
c9c9a149b6 | ||
|
|
0f9fdfe2de | ||
|
|
a76b63912d | ||
|
|
bc03e13d38 | ||
|
|
450aa9757d | ||
|
|
158389a4f2 | ||
|
|
95e89d5ef1 | ||
|
|
e107b8e5cd | ||
|
|
f875b43ede | ||
|
|
6242ef78c4 | ||
|
|
3c342c0768 | ||
|
|
5dba5fc79d | ||
|
|
713b7cf36d | ||
|
|
cb016b014b | ||
|
|
afb4523f63 | ||
|
|
05ad4986ac | ||
|
|
42dbd5f98f | ||
|
|
f58a514ce7 | ||
|
|
8fb384a5e1 | ||
|
|
c24302b5ce | ||
|
|
999ad9b642 | ||
|
|
36d6b4dafe | ||
|
|
06870a2e25 | ||
|
|
85eba2bb15 | ||
|
|
5dd6dcc215 | ||
|
|
8bf894a514 | ||
|
|
d3c67f2ae1 | ||
|
|
b60a282b60 | ||
|
|
0da1d40a19 | ||
|
|
aa3be915a0 | ||
|
|
0d97bfbc59 | ||
|
|
fe830337c9 | ||
|
|
5210b7d847 | ||
|
|
2f7ed4040b | ||
|
|
6376ba93a7 | ||
|
|
fd3a1cc9f4 | ||
|
|
208013ab76 | ||
|
|
770b3f910e | ||
|
|
5dce4a8eda | ||
|
|
6fcc9da948 | ||
|
|
bf93580ff9 | ||
|
|
0c2fe045d5 | ||
|
|
e14a3a6b0e | ||
|
|
e032740e90 | ||
|
|
78ad1e102d | ||
|
|
4f97cc7b68 | ||
|
|
df8f135532 | ||
|
|
0066801b0f | ||
|
|
0aa66ed6cb | ||
|
|
6903463f14 | ||
|
|
a473010fee | ||
|
|
ddf7a783a8 | ||
|
|
513e4d52fe | ||
|
|
17bb14e260 | ||
|
|
cd1258464b | ||
|
|
d3f5e0e6d7 |
46
.claude/skills/github-pr-reviewer/SKILL.md
Normal file
46
.claude/skills/github-pr-reviewer/SKILL.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: github-pr-reviewer
|
||||
description: Review a GitHub pull request and provide feedback comments. Use when the user says "review the current PR" or asks to review a specific PR.
|
||||
---
|
||||
|
||||
# Review GitHub Pull Request
|
||||
|
||||
## Preparation:
|
||||
- Check if the local commit matches the last one in the PR. If not, checkout the PR locally using 'gh pr checkout'.
|
||||
- CRITICAL: If 'gh pr checkout' fails for ANY reason, you MUST immediately STOP.
|
||||
- Do NOT attempt any workarounds.
|
||||
- Do NOT proceed with the review.
|
||||
- ALERT about the failure and WAIT for instructions.
|
||||
- This is a hard requirement - no exceptions.
|
||||
|
||||
## Follow these steps:
|
||||
1. Use 'gh pr view' to get the PR details and description.
|
||||
2. Use 'gh pr diff' to see all the changes in the PR.
|
||||
3. Analyze the code changes for:
|
||||
- Code quality and style consistency
|
||||
- Potential bugs or issues
|
||||
- Performance implications
|
||||
- Security concerns
|
||||
- Test coverage
|
||||
- Documentation updates if needed
|
||||
4. Ensure any existing review comments have been addressed.
|
||||
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
|
||||
|
||||
## IMPORTANT:
|
||||
- Just review. DO NOT make any changes
|
||||
- Be constructive and specific in your comments
|
||||
- Suggest improvements where appropriate
|
||||
- Only provide review feedback in the CONSOLE. DO NOT ACT ON GITHUB.
|
||||
- No need to run tests or linters, just review the code changes.
|
||||
- No need to highlight things that are already good.
|
||||
|
||||
## Output format:
|
||||
- List specific comments for each file/line that needs attention
|
||||
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
|
||||
- Example output:
|
||||
```
|
||||
Overall assessment: request changes.
|
||||
- [CRITICAL] Memory leak in homeassistant/components/sensor/my_sensor.py:143
|
||||
- [PROBLEM] Inefficient algorithm in homeassistant/helpers/data_processing.py:87
|
||||
- [SUGGESTION] Improve variable naming in homeassistant/helpers/config_validation.py:45
|
||||
```
|
||||
1180
.github/copilot-instructions.md
vendored
1180
.github/copilot-instructions.md
vendored
File diff suppressed because it is too large
Load Diff
35
.github/workflows/builder.yml
vendored
35
.github/workflows/builder.yml
vendored
@@ -10,7 +10,6 @@ on:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: core
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
PIP_TIMEOUT: 60
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
@@ -42,10 +41,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Get information
|
||||
id: info
|
||||
@@ -80,7 +79,7 @@ jobs:
|
||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
@@ -112,7 +111,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
|
||||
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -123,7 +122,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
|
||||
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: OHF-Voice/intents-package
|
||||
@@ -132,11 +131,11 @@ jobs:
|
||||
workflow_conclusion: success
|
||||
name: package
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Adjust nightly version
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
@@ -182,7 +181,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -197,7 +196,7 @@ jobs:
|
||||
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -329,7 +328,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -407,13 +406,13 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -538,13 +537,13 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -586,7 +585,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -615,7 +614,7 @@ jobs:
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0
|
||||
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
|
||||
92
.github/workflows/ci.yaml
vendored
92
.github/workflows/ci.yaml
vendored
@@ -41,8 +41,7 @@ env:
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.4"
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
ALL_PYTHON_VERSIONS: "['3.14.2']"
|
||||
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
||||
# 10.3 is the oldest supported version
|
||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||
# 10.6 is the current long-term-support
|
||||
@@ -166,6 +165,11 @@ jobs:
|
||||
tests_glob=""
|
||||
lint_only=""
|
||||
skip_coverage=""
|
||||
default_python=$(cat .python-version)
|
||||
all_python_versions=$(jq -cn \
|
||||
--arg default_python "${default_python}" \
|
||||
--argjson additional_python_versions "${ADDITIONAL_PYTHON_VERSIONS}" \
|
||||
'[$default_python] + $additional_python_versions')
|
||||
|
||||
if [[ "${INTEGRATION_CHANGES}" != "[]" ]];
|
||||
then
|
||||
@@ -235,8 +239,8 @@ jobs:
|
||||
echo "mariadb_groups=${mariadb_groups}" >> $GITHUB_OUTPUT
|
||||
echo "postgresql_groups: ${postgresql_groups}"
|
||||
echo "postgresql_groups=${postgresql_groups}" >> $GITHUB_OUTPUT
|
||||
echo "python_versions: ${ALL_PYTHON_VERSIONS}"
|
||||
echo "python_versions=${ALL_PYTHON_VERSIONS}" >> $GITHUB_OUTPUT
|
||||
echo "python_versions: ${all_python_versions}"
|
||||
echo "python_versions=${all_python_versions}" >> $GITHUB_OUTPUT
|
||||
echo "test_full_suite: ${test_full_suite}"
|
||||
echo "test_full_suite=${test_full_suite}" >> $GITHUB_OUTPUT
|
||||
echo "integrations_glob: ${integrations_glob}"
|
||||
@@ -452,7 +456,7 @@ jobs:
|
||||
python --version
|
||||
uv pip freeze >> pip_freeze.txt
|
||||
- name: Upload pip_freeze artifact
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: pip-freeze-${{ matrix.python-version }}
|
||||
path: pip_freeze.txt
|
||||
@@ -503,13 +507,13 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
@@ -540,13 +544,13 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
@@ -576,11 +580,11 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Run gen_copilot_instructions.py
|
||||
run: |
|
||||
@@ -605,7 +609,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
|
||||
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
|
||||
with:
|
||||
license-check: false # We use our own license audit checks
|
||||
|
||||
@@ -653,7 +657,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.licenses extract --output-file=licenses-${PYTHON_VERSION}.json
|
||||
- name: Upload licenses
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
||||
path: licenses-${{ matrix.python-version }}.json
|
||||
@@ -682,13 +686,13 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
@@ -735,13 +739,13 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
@@ -786,11 +790,11 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Generate partial mypy restore key
|
||||
id: generate-mypy-key
|
||||
@@ -798,7 +802,7 @@ jobs:
|
||||
mypy_version=$(cat requirements_test.txt | grep 'mypy.*=' | cut -d '=' -f 3)
|
||||
echo "version=${mypy_version}" >> $GITHUB_OUTPUT
|
||||
echo "key=mypy-${MYPY_CACHE_VERSION}-${mypy_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
@@ -879,13 +883,13 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
@@ -901,7 +905,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.split_tests ${TEST_GROUP_COUNT} tests
|
||||
- name: Upload pytest_buckets
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: pytest_buckets
|
||||
path: pytest_buckets.txt
|
||||
@@ -978,7 +982,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: pytest_buckets
|
||||
- name: Compile English translations
|
||||
@@ -1020,14 +1024,14 @@ jobs:
|
||||
2>&1 | tee pytest-${PYTHON_VERSION}-${TEST_GROUP}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: pytest-*.txt
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
@@ -1040,7 +1044,7 @@ jobs:
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: test-results-full-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: junit.xml
|
||||
@@ -1177,7 +1181,7 @@ jobs:
|
||||
2>&1 | tee pytest-${PYTHON_VERSION}-${mariadb}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -1185,7 +1189,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -1199,7 +1203,7 @@ jobs:
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: test-results-mariadb-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -1338,7 +1342,7 @@ jobs:
|
||||
2>&1 | tee pytest-${PYTHON_VERSION}-${postgresql}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1346,7 +1350,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1360,7 +1364,7 @@ jobs:
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: test-results-postgres-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1387,7 +1391,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -1514,14 +1518,14 @@ jobs:
|
||||
2>&1 | tee pytest-${PYTHON_VERSION}-${TEST_GROUP}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: pytest-*.txt
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
@@ -1534,7 +1538,7 @@ jobs:
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: test-results-partial-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: junit.xml
|
||||
@@ -1558,7 +1562,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -1587,7 +1591,7 @@ jobs:
|
||||
&& needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -28,11 +28,11 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -236,7 +236,7 @@ jobs:
|
||||
- name: Detect duplicates using AI
|
||||
id: ai_detection
|
||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||
uses: actions/ai-inference@a380166897b5408b8fb7dddd148142794cb5624a # v2.0.6
|
||||
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt: |
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
- name: Detect language using AI
|
||||
id: ai_language_detection
|
||||
if: steps.detect_language.outputs.should_continue == 'true'
|
||||
uses: actions/ai-inference@a380166897b5408b8fb7dddd148142794cb5624a # v2.0.6
|
||||
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
|
||||
with:
|
||||
model: openai/gpt-4o-mini
|
||||
system-prompt: |
|
||||
|
||||
7
.github/workflows/translations.yml
vendored
7
.github/workflows/translations.yml
vendored
@@ -15,9 +15,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
name: Upload
|
||||
@@ -29,10 +26,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Upload Translations
|
||||
env:
|
||||
|
||||
25
.github/workflows/wheels.yml
vendored
25
.github/workflows/wheels.yml
vendored
@@ -16,9 +16,6 @@ on:
|
||||
- "requirements.txt"
|
||||
- "script/gen_requirements_all.py"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
@@ -36,11 +33,11 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
|
||||
- name: Create Python virtual environment
|
||||
@@ -77,7 +74,7 @@ jobs:
|
||||
) > .env_file
|
||||
|
||||
- name: Upload env_file
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
@@ -85,7 +82,7 @@ jobs:
|
||||
overwrite: true
|
||||
|
||||
- name: Upload requirements_diff
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
path: ./requirements_diff.txt
|
||||
@@ -97,7 +94,7 @@ jobs:
|
||||
python -m script.gen_requirements_all ci
|
||||
|
||||
- name: Upload requirements_all_wheels
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
path: ./requirements_all_wheels_*.txt
|
||||
@@ -124,12 +121,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@@ -175,17 +172,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
@@ -209,4 +206,4 @@ jobs:
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txt"
|
||||
requirements: "requirements_all_wheels_${{ matrix.arch }}.txt"
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.14
|
||||
3.14.2
|
||||
|
||||
@@ -123,7 +123,6 @@ homeassistant.components.blueprint.*
|
||||
homeassistant.components.bluesound.*
|
||||
homeassistant.components.bluetooth.*
|
||||
homeassistant.components.bluetooth_adapters.*
|
||||
homeassistant.components.bmw_connected_drive.*
|
||||
homeassistant.components.bond.*
|
||||
homeassistant.components.bosch_alarm.*
|
||||
homeassistant.components.braviatv.*
|
||||
@@ -213,6 +212,7 @@ homeassistant.components.flexit_bacnet.*
|
||||
homeassistant.components.flux_led.*
|
||||
homeassistant.components.folder_watcher.*
|
||||
homeassistant.components.forecast_solar.*
|
||||
homeassistant.components.freshr.*
|
||||
homeassistant.components.fritz.*
|
||||
homeassistant.components.fritzbox.*
|
||||
homeassistant.components.fritzbox_callmonitor.*
|
||||
@@ -342,6 +342,7 @@ homeassistant.components.lookin.*
|
||||
homeassistant.components.lovelace.*
|
||||
homeassistant.components.luftdaten.*
|
||||
homeassistant.components.lunatone.*
|
||||
homeassistant.components.lutron.*
|
||||
homeassistant.components.madvr.*
|
||||
homeassistant.components.manual.*
|
||||
homeassistant.components.mastodon.*
|
||||
|
||||
318
AGENTS.md
318
AGENTS.md
@@ -4,325 +4,17 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
## Code Review Guidelines
|
||||
|
||||
**When reviewing code, do NOT comment on:**
|
||||
- **Missing imports** - We use static analysis tooling to catch that
|
||||
- **Code formatting** - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
|
||||
|
||||
**Git commit practices during review:**
|
||||
- **Do NOT amend, squash, or rebase commits after review has started** - Reviewers need to see what changed since their last review
|
||||
|
||||
## Python Requirements
|
||||
|
||||
- **Compatibility**: Python 3.13+
|
||||
- **Language Features**: Use the newest features when possible:
|
||||
- Pattern matching
|
||||
- Type hints
|
||||
- f-strings (preferred over `%` or `.format()`)
|
||||
- Dataclasses
|
||||
- Walrus operator
|
||||
|
||||
### Strict Typing (Platinum)
|
||||
- **Comprehensive Type Hints**: Add type hints to all functions, methods, and variables
|
||||
- **Custom Config Entry Types**: When using runtime_data:
|
||||
```python
|
||||
type MyIntegrationConfigEntry = ConfigEntry[MyClient]
|
||||
```
|
||||
- **Library Requirements**: Include `py.typed` file for PEP-561 compliance
|
||||
|
||||
## Code Quality Standards
|
||||
|
||||
- **Formatting**: Ruff
|
||||
- **Linting**: PyLint and Ruff
|
||||
- **Type Checking**: MyPy
|
||||
- **Lint/Type/Format Fixes**: Always prefer addressing the underlying issue (e.g., import the typed source, update shared stubs, align with Ruff expectations, or correct formatting at the source) before disabling a rule, adding `# type: ignore`, or skipping a formatter. Treat suppressions and `noqa` comments as a last resort once no compliant fix exists
|
||||
- **Testing**: pytest with plain functions and fixtures
|
||||
- **Language**: American English for all code, comments, and documentation (use sentence case, including titles)
|
||||
|
||||
### Writing Style Guidelines
|
||||
- **Tone**: Friendly and informative
|
||||
- **Perspective**: Use second-person ("you" and "your") for user-facing messages
|
||||
- **Inclusivity**: Use objective, non-discriminatory language
|
||||
- **Clarity**: Write for non-native English speakers
|
||||
- **Formatting in Messages**:
|
||||
- Use backticks for: file paths, filenames, variable names, field entries
|
||||
- Use sentence case for titles and messages (capitalize only the first word and proper nouns)
|
||||
- Avoid abbreviations when possible
|
||||
|
||||
### Documentation Standards
|
||||
- **File Headers**: Short and concise
|
||||
```python
|
||||
"""Integration for Peblar EV chargers."""
|
||||
```
|
||||
- **Method/Function Docstrings**: Required for all
|
||||
```python
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bool:
|
||||
"""Set up Peblar from a config entry."""
|
||||
```
|
||||
- **Comment Style**:
|
||||
- Use clear, descriptive comments
|
||||
- Explain the "why" not just the "what"
|
||||
- Keep code block lines under 80 characters when possible
|
||||
- Use progressive disclosure (simple explanation first, complex details later)
|
||||
|
||||
## Async Programming
|
||||
|
||||
- All external I/O operations must be async
|
||||
- **Best Practices**:
|
||||
- Avoid sleeping in loops
|
||||
- Avoid awaiting in loops - use `gather` instead
|
||||
- No blocking calls
|
||||
- Group executor jobs when possible - switching between event loop and executor is expensive
|
||||
|
||||
### Blocking Operations
|
||||
- **Use Executor**: For blocking I/O operations
|
||||
```python
|
||||
result = await hass.async_add_executor_job(blocking_function, args)
|
||||
```
|
||||
- **Never Block Event Loop**: Avoid file operations, `time.sleep()`, blocking HTTP calls
|
||||
- **Replace with Async**: Use `asyncio.sleep()` instead of `time.sleep()`
|
||||
|
||||
### Thread Safety
|
||||
- **@callback Decorator**: For event loop safe functions
|
||||
```python
|
||||
@callback
|
||||
def async_update_callback(self, event):
|
||||
"""Safe to run in event loop."""
|
||||
self.async_write_ha_state()
|
||||
```
|
||||
- **Sync APIs from Threads**: Use sync versions when calling from non-event loop threads
|
||||
- **Registry Changes**: Must be done in event loop thread
|
||||
|
||||
### Error Handling
|
||||
- **Exception Types**: Choose most specific exception available
|
||||
- `ServiceValidationError`: User input errors (preferred over `ValueError`)
|
||||
- `HomeAssistantError`: Device communication failures
|
||||
- `ConfigEntryNotReady`: Temporary setup issues (device offline)
|
||||
- `ConfigEntryAuthFailed`: Authentication problems
|
||||
- `ConfigEntryError`: Permanent setup issues
|
||||
- **Try/Catch Best Practices**:
|
||||
- Only wrap code that can throw exceptions
|
||||
- Keep try blocks minimal - process data after the try/catch
|
||||
- **Avoid bare exceptions** except in specific cases:
|
||||
- ❌ Generally not allowed: `except:` or `except Exception:`
|
||||
- ✅ Allowed in config flows to ensure robustness
|
||||
- ✅ Allowed in functions/methods that run in background tasks
|
||||
- Bad pattern:
|
||||
```python
|
||||
try:
|
||||
data = await device.get_data() # Can throw
|
||||
# ❌ Don't process data inside try block
|
||||
processed = data.get("value", 0) * 100
|
||||
self._attr_native_value = processed
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed to get data")
|
||||
```
|
||||
- Good pattern:
|
||||
```python
|
||||
try:
|
||||
data = await device.get_data() # Can throw
|
||||
except DeviceError:
|
||||
_LOGGER.error("Failed to get data")
|
||||
return
|
||||
|
||||
# ✅ Process data outside try block
|
||||
processed = data.get("value", 0) * 100
|
||||
self._attr_native_value = processed
|
||||
```
|
||||
- **Bare Exception Usage**:
|
||||
```python
|
||||
# ❌ Not allowed in regular code
|
||||
try:
|
||||
data = await device.get_data()
|
||||
except Exception: # Too broad
|
||||
_LOGGER.error("Failed")
|
||||
|
||||
# ✅ Allowed in config flow for robustness
|
||||
async def async_step_user(self, user_input=None):
|
||||
try:
|
||||
await self._test_connection(user_input)
|
||||
except Exception: # Allowed here
|
||||
errors["base"] = "unknown"
|
||||
|
||||
# ✅ Allowed in background tasks
|
||||
async def _background_refresh():
|
||||
try:
|
||||
await coordinator.async_refresh()
|
||||
except Exception: # Allowed in task
|
||||
_LOGGER.exception("Unexpected error in background task")
|
||||
```
|
||||
- **Setup Failure Patterns**:
|
||||
```python
|
||||
try:
|
||||
await device.async_setup()
|
||||
except (asyncio.TimeoutError, TimeoutException) as ex:
|
||||
raise ConfigEntryNotReady(f"Timeout connecting to {device.host}") from ex
|
||||
except AuthFailed as ex:
|
||||
raise ConfigEntryAuthFailed(f"Credentials expired for {device.name}") from ex
|
||||
```
|
||||
|
||||
### Logging
|
||||
- **Format Guidelines**:
|
||||
- No periods at end of messages
|
||||
- No integration names/domains (added automatically)
|
||||
- No sensitive data (keys, tokens, passwords)
|
||||
- Use debug level for non-user-facing messages
|
||||
- **Use Lazy Logging**:
|
||||
```python
|
||||
_LOGGER.debug("This is a log message with %s", variable)
|
||||
```
|
||||
|
||||
### Unavailability Logging
|
||||
- **Log Once**: When device/service becomes unavailable (info level)
|
||||
- **Log Recovery**: When device/service comes back online
|
||||
- **Implementation Pattern**:
|
||||
```python
|
||||
_unavailable_logged: bool = False
|
||||
|
||||
if not self._unavailable_logged:
|
||||
_LOGGER.info("The sensor is unavailable: %s", ex)
|
||||
self._unavailable_logged = True
|
||||
# On recovery:
|
||||
if self._unavailable_logged:
|
||||
_LOGGER.info("The sensor is back online")
|
||||
self._unavailable_logged = False
|
||||
```
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Environment
|
||||
- **Local development (non-container)**: Activate the project venv before running commands: `source .venv/bin/activate`
|
||||
- **Dev container**: No activation needed, the environment is pre-configured
|
||||
.vscode/tasks.json contains useful commands used for development.
|
||||
|
||||
### Code Quality & Linting
|
||||
- **Run all linters on all files**: `prek run --all-files`
|
||||
- **Run linters on staged files only**: `prek run`
|
||||
- **PyLint on everything** (slow): `pylint homeassistant`
|
||||
- **PyLint on specific folder**: `pylint homeassistant/components/my_integration`
|
||||
- **MyPy type checking (whole project)**: `mypy homeassistant/`
|
||||
- **MyPy on specific integration**: `mypy homeassistant/components/my_integration`
|
||||
## Python Syntax Notes
|
||||
|
||||
### Testing
|
||||
- **Quick test of changed files**: `pytest --timeout=10 --picked`
|
||||
- **Update test snapshots**: Add `--snapshot-update` to pytest command
|
||||
- ⚠️ Omit test results after using `--snapshot-update`
|
||||
- Always run tests again without the flag to verify snapshots
|
||||
- **Full test suite** (AVOID - very slow): `pytest ./tests`
|
||||
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses.
|
||||
|
||||
### Dependencies & Requirements
|
||||
- **Update generated files after dependency changes**: `python -m script.gen_requirements_all`
|
||||
- **Install all Python requirements**:
|
||||
```bash
|
||||
uv pip install -r requirements_all.txt -r requirements.txt -r requirements_test.txt
|
||||
```
|
||||
- **Install test requirements only**:
|
||||
```bash
|
||||
uv pip install -r requirements_test_all.txt -r requirements.txt
|
||||
```
|
||||
## Good practices
|
||||
|
||||
### Translations
|
||||
- **Update translations after strings.json changes**:
|
||||
```bash
|
||||
python -m script.translations develop --all
|
||||
```
|
||||
|
||||
### Project Validation
|
||||
- **Run hassfest** (checks project structure and updates generated files):
|
||||
```bash
|
||||
python -m script.hassfest
|
||||
```
|
||||
|
||||
## Common Anti-Patterns & Best Practices
|
||||
|
||||
### ❌ **Avoid These Patterns**
|
||||
```python
|
||||
# Blocking operations in event loop
|
||||
data = requests.get(url) # ❌ Blocks event loop
|
||||
time.sleep(5) # ❌ Blocks event loop
|
||||
|
||||
# Reusing BleakClient instances
|
||||
self.client = BleakClient(address)
|
||||
await self.client.connect()
|
||||
# Later...
|
||||
await self.client.connect() # ❌ Don't reuse
|
||||
|
||||
# Hardcoded strings in code
|
||||
self._attr_name = "Temperature Sensor" # ❌ Not translatable
|
||||
|
||||
# Missing error handling
|
||||
data = await self.api.get_data() # ❌ No exception handling
|
||||
|
||||
# Storing sensitive data in diagnostics
|
||||
return {"api_key": entry.data[CONF_API_KEY]} # ❌ Exposes secrets
|
||||
|
||||
# Accessing hass.data directly in tests
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id] # ❌ Don't access hass.data
|
||||
|
||||
# User-configurable polling intervals
|
||||
# In config flow
|
||||
vol.Optional("scan_interval", default=60): cv.positive_int # ❌ Not allowed
|
||||
# In coordinator
|
||||
update_interval = timedelta(minutes=entry.data.get("scan_interval", 1)) # ❌ Not allowed
|
||||
|
||||
# User-configurable config entry names (non-helper integrations)
|
||||
vol.Optional("name", default="My Device"): cv.string # ❌ Not allowed in regular integrations
|
||||
|
||||
# Too much code in try block
|
||||
try:
|
||||
response = await client.get_data() # Can throw
|
||||
# ❌ Data processing should be outside try block
|
||||
temperature = response["temperature"] / 10
|
||||
humidity = response["humidity"]
|
||||
self._attr_native_value = temperature
|
||||
except ClientError:
|
||||
_LOGGER.error("Failed to fetch data")
|
||||
|
||||
# Bare exceptions in regular code
|
||||
try:
|
||||
value = await sensor.read_value()
|
||||
except Exception: # ❌ Too broad - catch specific exceptions
|
||||
_LOGGER.error("Failed to read sensor")
|
||||
```
|
||||
|
||||
### ✅ **Use These Patterns Instead**
|
||||
```python
|
||||
# Async operations with executor
|
||||
data = await hass.async_add_executor_job(requests.get, url)
|
||||
await asyncio.sleep(5) # ✅ Non-blocking
|
||||
|
||||
# Fresh BleakClient instances
|
||||
client = BleakClient(address) # ✅ New instance each time
|
||||
await client.connect()
|
||||
|
||||
# Translatable entity names
|
||||
_attr_translation_key = "temperature_sensor" # ✅ Translatable
|
||||
|
||||
# Proper error handling
|
||||
try:
|
||||
data = await self.api.get_data()
|
||||
except ApiException as err:
|
||||
raise UpdateFailed(f"API error: {err}") from err
|
||||
|
||||
# Redacted diagnostics data
|
||||
return async_redact_data(data, {"api_key", "password"}) # ✅ Safe
|
||||
|
||||
# Test through proper integration setup and fixtures
|
||||
@pytest.fixture
|
||||
async def init_integration(hass, mock_config_entry, mock_api):
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id) # ✅ Proper setup
|
||||
|
||||
# Integration-determined polling intervals (not user-configurable)
|
||||
SCAN_INTERVAL = timedelta(minutes=5) # ✅ Common pattern: constant in const.py
|
||||
|
||||
class MyCoordinator(DataUpdateCoordinator[MyData]):
|
||||
def __init__(self, hass: HomeAssistant, client: MyClient, config_entry: ConfigEntry) -> None:
|
||||
# ✅ Integration determines interval based on device capabilities, connection type, etc.
|
||||
interval = timedelta(minutes=1) if client.is_local else SCAN_INTERVAL
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=interval,
|
||||
config_entry=config_entry, # ✅ Pass config_entry - it's accepted and recommended
|
||||
)
|
||||
```
|
||||
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
|
||||
|
||||
29
CODEOWNERS
generated
29
CODEOWNERS
generated
@@ -234,8 +234,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/bluetooth/ @bdraco
|
||||
/homeassistant/components/bluetooth_adapters/ @bdraco
|
||||
/tests/components/bluetooth_adapters/ @bdraco
|
||||
/homeassistant/components/bmw_connected_drive/ @gerard33 @rikroe
|
||||
/tests/components/bmw_connected_drive/ @gerard33 @rikroe
|
||||
/homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
|
||||
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
|
||||
/homeassistant/components/bosch_alarm/ @mag1024 @sanjay900
|
||||
@@ -281,6 +279,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/cert_expiry/ @jjlawren
|
||||
/homeassistant/components/chacon_dio/ @cnico
|
||||
/tests/components/chacon_dio/ @cnico
|
||||
/homeassistant/components/chess_com/ @joostlek
|
||||
/tests/components/chess_com/ @joostlek
|
||||
/homeassistant/components/cisco_ios/ @fbradyirl
|
||||
/homeassistant/components/cisco_mobility_express/ @fbradyirl
|
||||
/homeassistant/components/cisco_webex_teams/ @fbradyirl
|
||||
@@ -383,6 +383,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/dlna_dms/ @chishm
|
||||
/homeassistant/components/dnsip/ @gjohansson-ST
|
||||
/tests/components/dnsip/ @gjohansson-ST
|
||||
/homeassistant/components/door/ @home-assistant/core
|
||||
/tests/components/door/ @home-assistant/core
|
||||
/homeassistant/components/doorbird/ @oblogic7 @bdraco @flacjacket
|
||||
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
|
||||
/homeassistant/components/dormakaba_dkey/ @emontnemery
|
||||
@@ -549,6 +551,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/freebox/ @hacf-fr @Quentame
|
||||
/homeassistant/components/freedompro/ @stefano055415
|
||||
/tests/components/freedompro/ @stefano055415
|
||||
/homeassistant/components/freshr/ @SierraNL
|
||||
/tests/components/freshr/ @SierraNL
|
||||
/homeassistant/components/fressnapf_tracker/ @eifinger
|
||||
/tests/components/fressnapf_tracker/ @eifinger
|
||||
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||
@@ -567,10 +571,14 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/fully_kiosk/ @cgarwood
|
||||
/homeassistant/components/fyta/ @dontinelli
|
||||
/tests/components/fyta/ @dontinelli
|
||||
/homeassistant/components/garage_door/ @home-assistant/core
|
||||
/tests/components/garage_door/ @home-assistant/core
|
||||
/homeassistant/components/garages_amsterdam/ @klaasnicolaas
|
||||
/tests/components/garages_amsterdam/ @klaasnicolaas
|
||||
/homeassistant/components/gardena_bluetooth/ @elupus
|
||||
/tests/components/gardena_bluetooth/ @elupus
|
||||
/homeassistant/components/gate/ @home-assistant/core
|
||||
/tests/components/gate/ @home-assistant/core
|
||||
/homeassistant/components/gdacs/ @exxamalte
|
||||
/tests/components/gdacs/ @exxamalte
|
||||
/homeassistant/components/generic/ @davet2001
|
||||
@@ -737,6 +745,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/huisbaasje/ @dennisschroer
|
||||
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
||||
/tests/components/humidifier/ @home-assistant/core @Shulyaka
|
||||
/homeassistant/components/humidity/ @home-assistant/core
|
||||
/tests/components/humidity/ @home-assistant/core
|
||||
/homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||
/homeassistant/components/husqvarna_automower/ @Thomas55555
|
||||
@@ -786,8 +796,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/improv_ble/ @emontnemery
|
||||
/homeassistant/components/incomfort/ @jbouwh
|
||||
/tests/components/incomfort/ @jbouwh
|
||||
/homeassistant/components/indevolt/ @xirtnl
|
||||
/tests/components/indevolt/ @xirtnl
|
||||
/homeassistant/components/indevolt/ @xirt
|
||||
/tests/components/indevolt/ @xirt
|
||||
/homeassistant/components/inels/ @epdevlab
|
||||
/tests/components/inels/ @epdevlab
|
||||
/homeassistant/components/influxdb/ @mdegat01 @Robbie1221
|
||||
@@ -1200,6 +1210,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/open_meteo/ @frenck
|
||||
/homeassistant/components/open_router/ @joostlek
|
||||
/tests/components/open_router/ @joostlek
|
||||
/homeassistant/components/opendisplay/ @g4bri3lDev
|
||||
/tests/components/opendisplay/ @g4bri3lDev
|
||||
/homeassistant/components/openerz/ @misialq
|
||||
/tests/components/openerz/ @misialq
|
||||
/homeassistant/components/openevse/ @c00w @firstof9
|
||||
@@ -1305,8 +1317,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/prosegur/ @dgomes
|
||||
/homeassistant/components/proximity/ @mib1185
|
||||
/tests/components/proximity/ @mib1185
|
||||
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno @erwindouna
|
||||
/tests/components/proxmoxve/ @jhollowe @Corbeno @erwindouna
|
||||
/homeassistant/components/proxmoxve/ @Corbeno @erwindouna @CoMPaTech
|
||||
/tests/components/proxmoxve/ @Corbeno @erwindouna @CoMPaTech
|
||||
/homeassistant/components/ps4/ @ktnrg45
|
||||
/tests/components/ps4/ @ktnrg45
|
||||
/homeassistant/components/pterodactyl/ @elmurato
|
||||
@@ -1650,8 +1662,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/system_bridge/ @timmo001
|
||||
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
||||
/tests/components/systemmonitor/ @gjohansson-ST
|
||||
/homeassistant/components/systemnexa2/ @konsulten @slangstrom
|
||||
/tests/components/systemnexa2/ @konsulten @slangstrom
|
||||
/homeassistant/components/systemnexa2/ @konsulten
|
||||
/tests/components/systemnexa2/ @konsulten
|
||||
/homeassistant/components/tado/ @erwindouna
|
||||
/tests/components/tado/ @erwindouna
|
||||
/homeassistant/components/tag/ @home-assistant/core
|
||||
@@ -1691,7 +1703,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tessie/ @Bre77
|
||||
/homeassistant/components/text/ @home-assistant/core
|
||||
/tests/components/text/ @home-assistant/core
|
||||
/homeassistant/components/tfiac/ @fredrike @mellado
|
||||
/homeassistant/components/thermobeacon/ @bdraco
|
||||
/tests/components/thermobeacon/ @bdraco
|
||||
/homeassistant/components/thermopro/ @bdraco @h3ss
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
# Create `config.yaml` For Config Flows
|
||||
|
||||
## Goal
|
||||
Document the persisted config entry and subentry payloads in each integration's `config.yaml` under `config_entry`, using selector-based field metadata that is consistent with Home Assistant selectors.
|
||||
|
||||
The output must describe what is **actually stored** in config entries (`data`, `options`, and `subentries`), not just what is shown in forms.
|
||||
|
||||
## Required Files Per Integration
|
||||
For each integration with `"config_flow": true` in `manifest.json`, inspect:
|
||||
|
||||
1. `config_flow.py`
|
||||
2. `__init__.py` (for migration and runtime usage confirmation)
|
||||
3. `const.py` (for `CONF_*`, version constants, and aliases)
|
||||
4. `strings.json` / translations only as fallback for field names not inferable from code
|
||||
5. Existing `config.yaml` (target file)
|
||||
|
||||
## Version Rules
|
||||
1. Default version is `major: 1`, `minor: 1` when no explicit version is defined.
|
||||
2. Read `VERSION` and `MINOR_VERSION` from the config flow class.
|
||||
3. If the class uses constants (for example `CONFIG_FLOW_VERSION`), resolve them from `const.py`.
|
||||
4. Document all known config-entry versions when code clearly supports multiple versions:
|
||||
- Current version from config flow class.
|
||||
- Historical versions from explicit migration branches (for example `async_migrate_entry` checks in `__init__.py`).
|
||||
5. Apply the same version logic to subentries (default `1.1` when unspecified).
|
||||
|
||||
## Storage Target Rules (Critical)
|
||||
Always determine where values are persisted:
|
||||
|
||||
1. `ConfigFlow.async_create_entry(data=...)` -> persisted in config entry `data`.
|
||||
2. `ConfigFlow.async_create_entry(..., options=...)` -> persisted in config entry `options`.
|
||||
3. `OptionsFlow.async_create_entry(data=...)` -> persisted in config entry `options`.
|
||||
4. `SchemaConfigFlowHandler` (default implementation):
|
||||
- Config flow values are stored in `options`.
|
||||
- Config entry `data` is empty.
|
||||
- Exception: class overrides `async_create_entry` (then follow override).
|
||||
5. `async_update_reload_and_abort(..., data=..., options=...)` updates existing entry payloads and must align with documented fields.
|
||||
|
||||
## Form-To-Storage Mapping Rules
|
||||
When `user_input` is stored directly, form schema must be mirrored in `config.yaml`.
|
||||
|
||||
### Config Flow
|
||||
If step logic returns `async_create_entry(data=user_input)`:
|
||||
1. Find the matching `async_show_form(..., data_schema=...)` for that step.
|
||||
2. Extract all schema keys.
|
||||
3. Add those keys to `config_entry.versions[*].data.fields`.
|
||||
|
||||
### Options Flow
|
||||
If options step returns `async_create_entry(data=user_input)`:
|
||||
1. Extract step schema keys.
|
||||
2. Add those keys to `config_entry.versions[*].options.fields`.
|
||||
|
||||
### Dict Payloads
|
||||
If `async_create_entry(data={...})` (or via a local dict variable/function that clearly returns a dict):
|
||||
1. Extract literal keys.
|
||||
2. Add keys to the relevant persisted section (`data` or `options`).
|
||||
|
||||
## Helper Flow Rules
|
||||
### `register_discovery_flow(...)`
|
||||
Creates entry with `data={}` by default. Keep data empty unless integration overrides flow behavior elsewhere.
|
||||
|
||||
### `register_webhook_flow(...)`
|
||||
Creates entry with:
|
||||
1. `webhook_id`
|
||||
2. `cloudhook`
|
||||
|
||||
These must be documented in `config_entry.versions[*].data.fields`.
|
||||
|
||||
### `AbstractOAuth2FlowHandler`
|
||||
Default OAuth payload includes:
|
||||
1. `auth_implementation`
|
||||
2. `token`
|
||||
|
||||
If integration overrides `async_oauth_create_entry` and adds additional stored keys, include those too.
|
||||
|
||||
## Subentry Rules
|
||||
1. Find `async_get_supported_subentry_types(...)` mapping and subentry flow classes (`ConfigSubentryFlow`).
|
||||
2. For each `subentry_type`, document under:
|
||||
- `config_entry.subentries.<subentry_type>.versions`
|
||||
3. Extract persisted subentry payload keys from:
|
||||
- `async_create_entry(data=...)` in subentry flow
|
||||
- direct subentry update calls with explicit data payloads
|
||||
4. Apply required/default/selector extraction exactly as for main config/option flows.
|
||||
|
||||
## Field Metadata Rules
|
||||
Each field entry should include:
|
||||
1. `required` (true/false)
|
||||
2. `selector` (valid HA selector structure)
|
||||
3. Optional `default` and `example` when directly known from code
|
||||
|
||||
### Required Flag
|
||||
1. `vol.Required(...)` -> `required: true`
|
||||
2. `vol.Optional(...)` -> `required: false`
|
||||
3. Literal dict payloads without schema context -> `required: true` unless clearly optional in code path
|
||||
|
||||
### Selector Mapping
|
||||
Use explicit selector calls when present (for example `TextSelector`, `NumberSelector`, `BooleanSelector`, `LocationSelector`, `SelectSelector`, etc).
|
||||
|
||||
If schema uses plain validators:
|
||||
1. `bool` / `cv.boolean` -> `selector: { boolean: {} }`
|
||||
2. numeric validators -> `selector: { number: {} }`
|
||||
3. `vol.In(...)` / constrained choices -> `selector: { select: {} }`
|
||||
4. unknown / string-like -> `selector: { text: {} }`
|
||||
5. structured blobs (for example OAuth `token`) -> `selector: { object: {} }`
|
||||
|
||||
## Validation Checklist (Per Integration)
|
||||
1. `config.yaml` exists when `manifest.json` has `config_flow: true`.
|
||||
2. `config_entry.versions` contains correct version entries.
|
||||
3. Documented fields exactly match persisted payloads (`data` vs `options`).
|
||||
4. `required` and selector format are valid.
|
||||
5. `subentries` are documented when supported.
|
||||
6. No placeholder empty blocks where code stores actual fields.
|
||||
|
||||
## Final QA Commands
|
||||
Run after updates:
|
||||
|
||||
```bash
|
||||
python -m script.hassfest -p config_entry --action validate
|
||||
ruff check script/hassfest/config_entry.py
|
||||
```
|
||||
|
||||
## High-Risk Pitfalls
|
||||
1. Assuming fields in forms are always stored in `data` (wrong for `SchemaConfigFlowHandler`).
|
||||
2. Missing fields when `data=user_input` is used with a non-empty schema.
|
||||
3. Skipping helper flows (`register_webhook_flow`, OAuth2 base handler behavior).
|
||||
4. Ignoring options/subentry flows that store separate payloads.
|
||||
5. Using placeholders instead of integration-specific field definitions.
|
||||
@@ -70,7 +70,7 @@ from .const import (
|
||||
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
||||
)
|
||||
from .core_config import async_process_ha_core_config
|
||||
from .exceptions import HomeAssistantError
|
||||
from .exceptions import HomeAssistantError, UnsupportedStorageVersionError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
category_registry,
|
||||
@@ -236,9 +236,20 @@ DEFAULT_INTEGRATIONS = {
|
||||
"input_text",
|
||||
"schedule",
|
||||
"timer",
|
||||
#
|
||||
# Base platforms:
|
||||
*BASE_PLATFORMS,
|
||||
#
|
||||
# Integrations providing triggers and conditions for base platforms:
|
||||
"door",
|
||||
"garage_door",
|
||||
"gate",
|
||||
"humidity",
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||
# These integrations are set up if recovery mode is activated.
|
||||
"backup",
|
||||
"cloud",
|
||||
"frontend",
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_SUPERVISOR = {
|
||||
@@ -433,32 +444,56 @@ def _init_blocking_io_modules_in_executor() -> None:
|
||||
is_docker_env()
|
||||
|
||||
|
||||
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||
"""Load the registries and modules that will do blocking I/O."""
|
||||
async def async_load_base_functionality(hass: core.HomeAssistant) -> bool:
|
||||
"""Load the registries and modules that will do blocking I/O.
|
||||
|
||||
Return whether loading succeeded.
|
||||
"""
|
||||
if DATA_REGISTRIES_LOADED in hass.data:
|
||||
return
|
||||
return True
|
||||
|
||||
hass.data[DATA_REGISTRIES_LOADED] = None
|
||||
entity.async_setup(hass)
|
||||
frame.async_setup(hass)
|
||||
template.async_setup(hass)
|
||||
translation.async_setup(hass)
|
||||
await asyncio.gather(
|
||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||
create_eager_task(area_registry.async_load(hass)),
|
||||
create_eager_task(category_registry.async_load(hass)),
|
||||
create_eager_task(device_registry.async_load(hass)),
|
||||
create_eager_task(entity_registry.async_load(hass)),
|
||||
create_eager_task(floor_registry.async_load(hass)),
|
||||
create_eager_task(issue_registry.async_load(hass)),
|
||||
create_eager_task(label_registry.async_load(hass)),
|
||||
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
|
||||
create_eager_task(template.async_load_custom_templates(hass)),
|
||||
create_eager_task(restore_state.async_load(hass)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(condition.async_setup(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
|
||||
recovery = hass.config.recovery_mode
|
||||
try:
|
||||
await asyncio.gather(
|
||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||
create_eager_task(area_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(category_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(device_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(entity_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(floor_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(issue_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(label_registry.async_load(hass, load_empty=recovery)),
|
||||
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
|
||||
create_eager_task(template.async_load_custom_templates(hass)),
|
||||
create_eager_task(restore_state.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(condition.async_setup(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
except UnsupportedStorageVersionError as err:
|
||||
# If we're already in recovery mode, we don't want to handle the exception
|
||||
# and activate recovery mode again, as that would lead to an infinite loop.
|
||||
if recovery:
|
||||
raise
|
||||
|
||||
_LOGGER.error(
|
||||
"Storage file %s was created by a newer version of Home Assistant"
|
||||
" (storage version %s > %s); activating recovery mode; on-disk data"
|
||||
" is preserved; upgrade Home Assistant or restore from a backup",
|
||||
err.storage_key,
|
||||
err.found_version,
|
||||
err.max_supported_version,
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_from_config_dict(
|
||||
@@ -475,7 +510,9 @@ async def async_from_config_dict(
|
||||
# Prime custom component cache early so we know if registry entries are tied
|
||||
# to a custom integration
|
||||
await loader.async_get_custom_components(hass)
|
||||
await async_load_base_functionality(hass)
|
||||
|
||||
if not await async_load_base_functionality(hass):
|
||||
return None
|
||||
|
||||
# Set up core.
|
||||
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
|
||||
|
||||
5
homeassistant/brands/ubisys.json
Normal file
5
homeassistant/brands/ubisys.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "ubisys",
|
||||
"name": "Ubisys",
|
||||
"iot_standards": ["zigbee"]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
polling:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
is_new_style_scale:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,26 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
api_key:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
latitude:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
longitude:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
name:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -191,7 +191,7 @@ class AccuWeatherEntity(
|
||||
{
|
||||
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
|
||||
ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCoverDay"],
|
||||
ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"]["Average"],
|
||||
ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"].get("Average"),
|
||||
ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"][ATTR_VALUE],
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"][ATTR_VALUE],
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperatureMax"][
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
id:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
api_token:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
40
homeassistant/components/adax/climate.py
Normal file → Executable file
40
homeassistant/components/adax/climate.py
Normal file → Executable file
@@ -168,29 +168,57 @@ class LocalAdaxDevice(CoordinatorEntity[AdaxLocalCoordinator], ClimateEntity):
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
temperature = self._attr_target_temperature or self._attr_min_temp
|
||||
await self._adax_data_handler.set_target_temperature(temperature)
|
||||
self._attr_target_temperature = temperature
|
||||
self._attr_icon = "mdi:radiator"
|
||||
elif hvac_mode == HVACMode.OFF:
|
||||
await self._adax_data_handler.set_target_temperature(0)
|
||||
self._attr_icon = "mdi:radiator-off"
|
||||
else:
|
||||
# Ignore unsupported HVAC modes to avoid desynchronizing entity state
|
||||
# from the physical device.
|
||||
return
|
||||
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
return
|
||||
await self._adax_data_handler.set_target_temperature(temperature)
|
||||
if self._attr_hvac_mode == HVACMode.HEAT:
|
||||
await self._adax_data_handler.set_target_temperature(temperature)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._attr_target_temperature = temperature
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _update_hvac_attributes(self) -> None:
|
||||
"""Update hvac mode and temperatures from coordinator data.
|
||||
|
||||
The coordinator reports a target temperature of 0 when the heater is
|
||||
turned off. In that case, only the hvac mode and icon are updated and
|
||||
the previous non-zero target temperature is preserved. When the
|
||||
reported target temperature is non-zero, the stored target temperature
|
||||
is updated to match the coordinator value.
|
||||
"""
|
||||
if data := self.coordinator.data:
|
||||
self._attr_current_temperature = data["current_temperature"]
|
||||
self._attr_available = self._attr_current_temperature is not None
|
||||
if (target_temp := data["target_temperature"]) == 0:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_icon = "mdi:radiator-off"
|
||||
if target_temp == 0:
|
||||
if self._attr_target_temperature is None:
|
||||
self._attr_target_temperature = self._attr_min_temp
|
||||
else:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
self._attr_icon = "mdi:radiator"
|
||||
self._attr_target_temperature = target_temp
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._update_hvac_attributes()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self._update_hvac_attributes()
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 2
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
connection_type:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- Cloud
|
||||
- Local
|
||||
default: Cloud
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,34 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
password:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
ssl:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
username:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
verify_ssl:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
ip_address:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,34 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
api_key:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
latitude:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
longitude:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
name:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields:
|
||||
radar_updates:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
station_updates:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
subentries: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
api_key:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
server_url:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,26 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
api_key:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
latitude:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
longitude:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
name:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,30 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 2
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
api_key:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
latitude:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
longitude:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
radius:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields:
|
||||
radius:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
subentries: {}
|
||||
@@ -1,22 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
password:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
username:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,19 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 2
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
advanced_settings:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
mac_address:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
options: []
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
email:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
password:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,26 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
ip_address:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
password:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields:
|
||||
clip_negatives:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
return_average:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
subentries: {}
|
||||
@@ -18,6 +18,10 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaOptionsFlowHandler,
|
||||
)
|
||||
from homeassistant.helpers.selector import BooleanSelector
|
||||
from homeassistant.helpers.service_info.zeroconf import (
|
||||
ATTR_PROPERTIES_ID,
|
||||
ZeroconfServiceInfo,
|
||||
)
|
||||
|
||||
from .const import CONF_CLIP_NEGATIVE, CONF_RETURN_AVERAGE, DOMAIN
|
||||
|
||||
@@ -46,6 +50,9 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_discovered_host: str
|
||||
_discovered_name: str
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -90,6 +97,58 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery of an air-Q device."""
|
||||
self._discovered_host = discovery_info.host
|
||||
self._discovered_name = discovery_info.properties.get("devicename", "air-Q")
|
||||
device_id = discovery_info.properties.get(ATTR_PROPERTIES_ID)
|
||||
|
||||
if not device_id:
|
||||
return self.async_abort(reason="incomplete_discovery")
|
||||
|
||||
await self.async_set_unique_id(device_id)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_IP_ADDRESS: self._discovered_host},
|
||||
reload_on_update=True,
|
||||
)
|
||||
|
||||
self.context["title_placeholders"] = {"name": self._discovered_name}
|
||||
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle user confirmation of a discovered air-Q device."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
session = async_get_clientsession(self.hass)
|
||||
airq = AirQ(self._discovered_host, user_input[CONF_PASSWORD], session)
|
||||
try:
|
||||
await airq.validate()
|
||||
except ClientConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=self._discovered_name,
|
||||
data={
|
||||
CONF_IP_ADDRESS: self._discovered_host,
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
|
||||
description_placeholders={"name": self._discovered_name},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
|
||||
@@ -7,5 +7,13 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairq"],
|
||||
"requirements": ["aioairq==0.4.7"]
|
||||
"requirements": ["aioairq==0.4.7"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"properties": {
|
||||
"device": "air-q"
|
||||
},
|
||||
"type": "_http._tcp.local."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"incomplete_discovery": "The discovered air-Q device did not provide a device ID. Ensure the firmware is up to date."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_input": "[%key:common::config_flow::error::invalid_host%]"
|
||||
},
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"discovery_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"description": "Do you want to set up **{name}**?",
|
||||
"title": "Set up air-Q"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]",
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
secret:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
id:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
device_model:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -117,23 +117,23 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
|
||||
return super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
def current_temperature(self) -> int:
|
||||
"""Return the current temperature."""
|
||||
return self._unit.Temperature
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
def fan_mode(self) -> str:
|
||||
"""Return fan mode of the AC this group belongs to."""
|
||||
return AT_TO_HA_FAN_SPEED[self._airtouch.acs[self._ac_number].AcFanSpeed]
|
||||
|
||||
@property
|
||||
def fan_modes(self):
|
||||
def fan_modes(self) -> list[str]:
|
||||
"""Return the list of available fan modes."""
|
||||
airtouch_fan_speeds = self._airtouch.GetSupportedFanSpeedsForAc(self._ac_number)
|
||||
return [AT_TO_HA_FAN_SPEED[speed] for speed in airtouch_fan_speeds]
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return hvac target hvac state."""
|
||||
is_off = self._unit.PowerState == "Off"
|
||||
if is_off:
|
||||
@@ -236,17 +236,17 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
|
||||
return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
def current_temperature(self) -> int:
|
||||
"""Return the current temperature."""
|
||||
return self._unit.Temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
def target_temperature(self) -> int:
|
||||
"""Return the temperature we are trying to reach."""
|
||||
return self._unit.TargetSetpoint
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return hvac target hvac state."""
|
||||
# there are other power states that aren't 'on' but still count as on (eg. 'Turbo')
|
||||
is_off = self._unit.PowerState == "Off"
|
||||
@@ -272,12 +272,12 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
def fan_mode(self) -> str:
|
||||
"""Return fan mode of the AC this group belongs to."""
|
||||
return AT_TO_HA_FAN_SPEED[self._airtouch.acs[self._unit.BelongsToAc].AcFanSpeed]
|
||||
|
||||
@property
|
||||
def fan_modes(self):
|
||||
def fan_modes(self) -> list[str]:
|
||||
"""Return the list of available fan modes."""
|
||||
airtouch_fan_speeds = self._airtouch.GetSupportedFanSpeedsByGroup(
|
||||
self._group_number
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["airtouch5py"],
|
||||
"requirements": ["airtouch5py==0.3.0"]
|
||||
"requirements": ["airtouch5py==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -7,13 +7,7 @@ from datetime import timedelta
|
||||
from math import ceil
|
||||
from typing import Any
|
||||
|
||||
from pyairvisual.cloud_api import (
|
||||
CloudAPI,
|
||||
InvalidKeyError,
|
||||
KeyExpiredError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from pyairvisual.errors import AirVisualError
|
||||
from pyairvisual.cloud_api import CloudAPI
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
@@ -28,14 +22,12 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
CONF_CITY,
|
||||
@@ -47,8 +39,7 @@ from .const import (
|
||||
INTEGRATION_TYPE_NODE_PRO,
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
type AirVisualConfigEntry = ConfigEntry[DataUpdateCoordinator]
|
||||
from .coordinator import AirVisualConfigEntry, AirVisualDataUpdateCoordinator
|
||||
|
||||
# We use a raw string for the airvisual_pro domain (instead of importing the actual
|
||||
# constant) so that we can avoid listing it as a dependency:
|
||||
@@ -85,8 +76,8 @@ def async_get_cloud_api_update_interval(
|
||||
@callback
|
||||
def async_get_cloud_coordinators_by_api_key(
|
||||
hass: HomeAssistant, api_key: str
|
||||
) -> list[DataUpdateCoordinator]:
|
||||
"""Get all DataUpdateCoordinator objects related to a particular API key."""
|
||||
) -> list[AirVisualDataUpdateCoordinator]:
|
||||
"""Get all AirVisualDataUpdateCoordinator objects related to a particular API key."""
|
||||
return [
|
||||
entry.runtime_data
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
@@ -180,38 +171,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
cloud_api = CloudAPI(entry.data[CONF_API_KEY], session=websession)
|
||||
|
||||
async def async_update_data() -> dict[str, Any]:
|
||||
"""Get new data from the API."""
|
||||
if CONF_CITY in entry.data:
|
||||
api_coro = cloud_api.air_quality.city(
|
||||
entry.data[CONF_CITY],
|
||||
entry.data[CONF_STATE],
|
||||
entry.data[CONF_COUNTRY],
|
||||
)
|
||||
else:
|
||||
api_coro = cloud_api.air_quality.nearest_city(
|
||||
entry.data[CONF_LATITUDE],
|
||||
entry.data[CONF_LONGITUDE],
|
||||
)
|
||||
|
||||
try:
|
||||
return await api_coro
|
||||
except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex:
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
except AirVisualError as err:
|
||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
coordinator = AirVisualDataUpdateCoordinator(
|
||||
hass,
|
||||
LOGGER,
|
||||
config_entry=entry,
|
||||
entry,
|
||||
cloud_api,
|
||||
name=async_get_geography_id(entry.data),
|
||||
# We give a placeholder update interval in order to create the coordinator;
|
||||
# then, below, we use the coordinator's presence (along with any other
|
||||
# coordinators using the same API key) to calculate an actual, leveled
|
||||
# update interval:
|
||||
update_interval=timedelta(minutes=5),
|
||||
update_method=async_update_data,
|
||||
)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 3
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
integration_type:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields:
|
||||
show_on_map:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
subentries: {}
|
||||
72
homeassistant/components/airvisual/coordinator.py
Normal file
72
homeassistant/components/airvisual/coordinator.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Define an AirVisual data coordinator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from pyairvisual.cloud_api import (
|
||||
CloudAPI,
|
||||
InvalidKeyError,
|
||||
KeyExpiredError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from pyairvisual.errors import AirVisualError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_LATITUDE, CONF_LONGITUDE, CONF_STATE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_CITY, LOGGER
|
||||
|
||||
type AirVisualConfigEntry = ConfigEntry[AirVisualDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AirVisualDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Class to manage fetching AirVisual data."""
|
||||
|
||||
config_entry: AirVisualConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: AirVisualConfigEntry,
|
||||
cloud_api: CloudAPI,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self._cloud_api = cloud_api
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
config_entry=entry,
|
||||
name=name,
|
||||
# We give a placeholder update interval in order to create the coordinator;
|
||||
# then, in async_setup_entry, we use the coordinator's presence (along with
|
||||
# any other coordinators using the same API key) to calculate an actual,
|
||||
# leveled update interval:
|
||||
update_interval=timedelta(minutes=5),
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Get new data from the API."""
|
||||
if CONF_CITY in self.config_entry.data:
|
||||
api_coro = self._cloud_api.air_quality.city(
|
||||
self.config_entry.data[CONF_CITY],
|
||||
self.config_entry.data[CONF_STATE],
|
||||
self.config_entry.data[CONF_COUNTRY],
|
||||
)
|
||||
else:
|
||||
api_coro = self._cloud_api.air_quality.nearest_city(
|
||||
self.config_entry.data[CONF_LATITUDE],
|
||||
self.config_entry.data[CONF_LONGITUDE],
|
||||
)
|
||||
|
||||
try:
|
||||
return await api_coro
|
||||
except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex:
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
except AirVisualError as err:
|
||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||
@@ -15,8 +15,8 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AirVisualConfigEntry
|
||||
from .const import CONF_CITY
|
||||
from .coordinator import AirVisualConfigEntry
|
||||
|
||||
CONF_COORDINATES = "coordinates"
|
||||
CONF_TITLE = "title"
|
||||
|
||||
@@ -2,29 +2,25 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import AirVisualDataUpdateCoordinator
|
||||
|
||||
|
||||
class AirVisualEntity(CoordinatorEntity):
|
||||
class AirVisualEntity(CoordinatorEntity[AirVisualDataUpdateCoordinator]):
|
||||
"""Define a generic AirVisual entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
coordinator: AirVisualDataUpdateCoordinator,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_extra_state_attributes = {}
|
||||
self._entry = entry
|
||||
self.entity_description = description
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
|
||||
@@ -8,7 +8,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
@@ -24,10 +23,9 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import AirVisualConfigEntry
|
||||
from .const import CONF_CITY
|
||||
from .coordinator import AirVisualConfigEntry, AirVisualDataUpdateCoordinator
|
||||
from .entity import AirVisualEntity
|
||||
|
||||
ATTR_CITY = "city"
|
||||
@@ -113,7 +111,7 @@ async def async_setup_entry(
|
||||
"""Set up AirVisual sensors based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
AirVisualGeographySensor(coordinator, entry, description, locale)
|
||||
AirVisualGeographySensor(coordinator, description, locale)
|
||||
for locale in GEOGRAPHY_SENSOR_LOCALES
|
||||
for description in GEOGRAPHY_SENSOR_DESCRIPTIONS
|
||||
)
|
||||
@@ -124,14 +122,14 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
coordinator: AirVisualDataUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
locale: str,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, entry, description)
|
||||
super().__init__(coordinator, description)
|
||||
|
||||
entry = coordinator.config_entry
|
||||
self._attr_extra_state_attributes.update(
|
||||
{
|
||||
ATTR_CITY: entry.data.get(CONF_CITY),
|
||||
@@ -182,16 +180,16 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
|
||||
#
|
||||
# We use any coordinates in the config entry and, in the case of a geography by
|
||||
# name, we fall back to the latitude longitude provided in the coordinator data:
|
||||
latitude = self._entry.data.get(
|
||||
latitude = self.coordinator.config_entry.data.get(
|
||||
CONF_LATITUDE,
|
||||
self.coordinator.data["location"]["coordinates"][1],
|
||||
)
|
||||
longitude = self._entry.data.get(
|
||||
longitude = self.coordinator.config_entry.data.get(
|
||||
CONF_LONGITUDE,
|
||||
self.coordinator.data["location"]["coordinates"][0],
|
||||
)
|
||||
|
||||
if self._entry.options[CONF_SHOW_ON_MAP]:
|
||||
if self.coordinator.config_entry.options[CONF_SHOW_ON_MAP]:
|
||||
self._attr_extra_state_attributes[ATTR_LATITUDE] = latitude
|
||||
self._attr_extra_state_attributes[ATTR_LONGITUDE] = longitude
|
||||
self._attr_extra_state_attributes.pop("lati", None)
|
||||
|
||||
@@ -4,18 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from pyairvisual.node import (
|
||||
InvalidAuthenticationError,
|
||||
NodeConnectionError,
|
||||
NodeProError,
|
||||
NodeSamba,
|
||||
)
|
||||
from pyairvisual.node import NodeProError, NodeSamba
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_PASSWORD,
|
||||
@@ -23,25 +14,16 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import LOGGER
|
||||
from .coordinator import (
|
||||
AirVisualProConfigEntry,
|
||||
AirVisualProCoordinator,
|
||||
AirVisualProData,
|
||||
)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
UPDATE_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
type AirVisualProConfigEntry = ConfigEntry[AirVisualProData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AirVisualProData:
|
||||
"""Define a data class."""
|
||||
|
||||
coordinator: DataUpdateCoordinator
|
||||
node: NodeSamba
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AirVisualProConfigEntry
|
||||
@@ -54,48 +36,15 @@ async def async_setup_entry(
|
||||
except NodeProError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
reload_task: asyncio.Task | None = None
|
||||
|
||||
async def async_get_data() -> dict[str, Any]:
|
||||
"""Get data from the device."""
|
||||
try:
|
||||
data = await node.async_get_latest_measurements()
|
||||
data["history"] = {}
|
||||
if data["settings"].get("follow_mode") == "device":
|
||||
history = await node.async_get_history(include_trends=False)
|
||||
data["history"] = history.get("measurements", [])[-1]
|
||||
except InvalidAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed("Invalid Samba password") from err
|
||||
except NodeConnectionError as err:
|
||||
nonlocal reload_task
|
||||
if not reload_task:
|
||||
reload_task = hass.async_create_task(
|
||||
hass.config_entries.async_reload(entry.entry_id)
|
||||
)
|
||||
raise UpdateFailed(f"Connection to Pro unit lost: {err}") from err
|
||||
except NodeProError as err:
|
||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||
|
||||
return data
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
LOGGER,
|
||||
config_entry=entry,
|
||||
name="Node/Pro data",
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
update_method=async_get_data,
|
||||
)
|
||||
|
||||
coordinator = AirVisualProCoordinator(hass, entry, node)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = AirVisualProData(coordinator=coordinator, node=node)
|
||||
|
||||
async def async_shutdown(_: Event) -> None:
|
||||
"""Define an event handler to disconnect from the websocket."""
|
||||
nonlocal reload_task
|
||||
if reload_task:
|
||||
if coordinator.reload_task:
|
||||
with suppress(asyncio.CancelledError):
|
||||
reload_task.cancel()
|
||||
coordinator.reload_task.cancel()
|
||||
await node.async_disconnect()
|
||||
|
||||
entry.async_on_unload(
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
ip_address:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
password:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
79
homeassistant/components/airvisual_pro/coordinator.py
Normal file
79
homeassistant/components/airvisual_pro/coordinator.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""DataUpdateCoordinator for the AirVisual Pro integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from pyairvisual.node import (
|
||||
InvalidAuthenticationError,
|
||||
NodeConnectionError,
|
||||
NodeProError,
|
||||
NodeSamba,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import LOGGER
|
||||
|
||||
UPDATE_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AirVisualProData:
|
||||
"""Define a data class."""
|
||||
|
||||
coordinator: AirVisualProCoordinator
|
||||
node: NodeSamba
|
||||
|
||||
|
||||
type AirVisualProConfigEntry = ConfigEntry[AirVisualProData]
|
||||
|
||||
|
||||
class AirVisualProCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Coordinator for AirVisual Pro data."""
|
||||
|
||||
config_entry: AirVisualProConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: AirVisualProConfigEntry,
|
||||
node: NodeSamba,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
config_entry=config_entry,
|
||||
name="Node/Pro data",
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
)
|
||||
self._node = node
|
||||
self.reload_task: asyncio.Task[bool] | None = None
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Get data from the device."""
|
||||
try:
|
||||
data = await self._node.async_get_latest_measurements()
|
||||
data["history"] = {}
|
||||
if data["settings"].get("follow_mode") == "device":
|
||||
history = await self._node.async_get_history(include_trends=False)
|
||||
data["history"] = history.get("measurements", [])[-1]
|
||||
except InvalidAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed("Invalid Samba password") from err
|
||||
except NodeConnectionError as err:
|
||||
if self.reload_task is None:
|
||||
self.reload_task = self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||
)
|
||||
raise UpdateFailed(f"Connection to Pro unit lost: {err}") from err
|
||||
except NodeProError as err:
|
||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||
|
||||
return data
|
||||
@@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AirVisualProConfigEntry
|
||||
from .coordinator import AirVisualProConfigEntry
|
||||
|
||||
CONF_MAC_ADDRESS = "mac_address"
|
||||
CONF_SERIAL_NUMBER = "serial_number"
|
||||
|
||||
@@ -4,19 +4,17 @@ from __future__ import annotations
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirVisualProCoordinator
|
||||
|
||||
|
||||
class AirVisualProEntity(CoordinatorEntity):
|
||||
class AirVisualProEntity(CoordinatorEntity[AirVisualProCoordinator]):
|
||||
"""Define a generic AirVisual Pro entity."""
|
||||
|
||||
def __init__(
|
||||
self, coordinator: DataUpdateCoordinator, description: EntityDescription
|
||||
self, coordinator: AirVisualProCoordinator, description: EntityDescription
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
@@ -22,7 +22,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AirVisualProConfigEntry
|
||||
from .coordinator import AirVisualProConfigEntry
|
||||
from .entity import AirVisualProEntity
|
||||
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 2
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
id:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,22 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
id:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
password:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
username:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 2
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
auth_implementation:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
token:
|
||||
required: true
|
||||
selector:
|
||||
object: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -66,9 +66,7 @@ rules:
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices:
|
||||
status: todo
|
||||
comment: We can automatically remove removed devices
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
|
||||
@@ -44,7 +44,7 @@ def make_entity_state_trigger_required_features(
|
||||
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
_domain = domain
|
||||
_domains = {domain}
|
||||
_to_states = {to_state}
|
||||
_required_features = required_features
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
device_baudrate:
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
default: 115200
|
||||
device_path:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
default: /dev/ttyUSB0
|
||||
options:
|
||||
fields:
|
||||
arm_options:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
zone_options:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
subentries: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 3
|
||||
data:
|
||||
fields:
|
||||
login_data:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_MODEL
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_DEVICE_TYPE
|
||||
from aioamazondevices.structures import AmazonDevice
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -25,19 +25,20 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._serial_num = serial_num
|
||||
model_details = coordinator.api.get_model_details(self.device) or {}
|
||||
model = model_details.get("model")
|
||||
model = self.device.model
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_num)},
|
||||
name=self.device.account_name,
|
||||
model=model,
|
||||
model_id=self.device.device_type,
|
||||
manufacturer=model_details.get("manufacturer", "Amazon"),
|
||||
hw_version=model_details.get("hw_version"),
|
||||
manufacturer=self.device.manufacturer or "Amazon",
|
||||
hw_version=self.device.hardware_version,
|
||||
sw_version=(
|
||||
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
|
||||
self.device.software_version
|
||||
if model != SPEAKER_GROUP_DEVICE_TYPE
|
||||
else None
|
||||
),
|
||||
serial_number=serial_num if model != SPEAKER_GROUP_MODEL else None,
|
||||
serial_number=serial_num if model != SPEAKER_GROUP_DEVICE_TYPE else None,
|
||||
)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{serial_num}-{description.key}"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==12.0.0"]
|
||||
"requirements": ["aioamazondevices==13.0.0"]
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,16 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
site_id:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
options: []
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,17 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
station:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
multiple: false
|
||||
sort: true
|
||||
options: []
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 2
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
api_key:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
app_key:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,34 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 2
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
tracked_apps:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
tracked_custom_integrations:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
tracked_integrations:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields:
|
||||
tracked_apps:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
tracked_custom_integrations:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
tracked_integrations:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
subentries: {}
|
||||
@@ -1,26 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
password:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
username:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,77 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 2
|
||||
data:
|
||||
fields:
|
||||
adb_server_ip:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
adb_server_port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
adbkey:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
device_class:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields:
|
||||
app_delete:
|
||||
required: false
|
||||
selector:
|
||||
boolean: {}
|
||||
default: false
|
||||
apps:
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
options: []
|
||||
exclude_unnamed_apps:
|
||||
required: false
|
||||
selector:
|
||||
boolean: {}
|
||||
get_sources:
|
||||
required: false
|
||||
selector:
|
||||
boolean: {}
|
||||
rule_delete:
|
||||
required: false
|
||||
selector:
|
||||
boolean: {}
|
||||
default: false
|
||||
screencap_interval:
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
state_detection_rules:
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
options: []
|
||||
turn_off_command:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
turn_on_command:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
subentries: {}
|
||||
@@ -1,38 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
pin:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields:
|
||||
app_delete:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
app_icon:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
app_id:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
app_name:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
apps:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
enable_ime:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
subentries: {}
|
||||
@@ -27,4 +27,4 @@ def create_api(hass: HomeAssistant, host: str, enable_ime: bool) -> AndroidTVRem
|
||||
|
||||
def get_enable_ime(entry: AndroidTVRemoteConfigEntry) -> bool:
|
||||
"""Get value of enable_ime option or its default value."""
|
||||
return entry.options.get(CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE) # type: ignore[no-any-return]
|
||||
return bool(entry.options.get(CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE))
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
account_number:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
multiple: false
|
||||
options: []
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyanglianwater"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyanglianwater==3.1.0"]
|
||||
"requirements": ["pyanglianwater==3.1.1"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
|
||||
from pyanglianwater.meter import SmartMeter
|
||||
@@ -32,13 +33,14 @@ class AnglianWaterSensor(StrEnum):
|
||||
YESTERDAY_WATER_COST = "yesterday_water_cost"
|
||||
YESTERDAY_SEWERAGE_COST = "yesterday_sewerage_cost"
|
||||
LATEST_READING = "latest_reading"
|
||||
LAST_UPDATED = "last_updated"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AnglianWaterSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes AnglianWater sensor entity."""
|
||||
|
||||
value_fn: Callable[[SmartMeter], float]
|
||||
value_fn: Callable[[SmartMeter], float | datetime | None]
|
||||
|
||||
|
||||
ENTITY_DESCRIPTIONS: tuple[AnglianWaterSensorEntityDescription, ...] = (
|
||||
@@ -76,6 +78,13 @@ ENTITY_DESCRIPTIONS: tuple[AnglianWaterSensorEntityDescription, ...] = (
|
||||
translation_key=AnglianWaterSensor.YESTERDAY_SEWERAGE_COST,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AnglianWaterSensorEntityDescription(
|
||||
key=AnglianWaterSensor.LAST_UPDATED,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=lambda entity: entity.last_updated,
|
||||
translation_key=AnglianWaterSensor.LAST_UPDATED,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -112,6 +121,6 @@ class AnglianWaterSensorEntity(AnglianWaterEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
def native_value(self) -> float | datetime | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.smart_meter)
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"last_updated": {
|
||||
"name": "Last meter reading processed"
|
||||
},
|
||||
"latest_reading": {
|
||||
"name": "Latest reading"
|
||||
},
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 2
|
||||
data:
|
||||
fields:
|
||||
password:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
username:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,68 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 2
|
||||
minor: 3
|
||||
data:
|
||||
fields:
|
||||
api_key:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries:
|
||||
ai_task_data:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
chat_model:
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
custom_value: true
|
||||
options: []
|
||||
max_tokens:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
temperature:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.05
|
||||
options:
|
||||
fields: {}
|
||||
conversation:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
chat_model:
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
custom_value: true
|
||||
options: []
|
||||
max_tokens:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
temperature:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.05
|
||||
options:
|
||||
fields: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
email:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
password:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
device_input:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields:
|
||||
start_off:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
subentries: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
ip_address:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,23 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
brand:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
options: []
|
||||
refresh_token:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
refresh_token_creation_time:
|
||||
required: true
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
address:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -8,19 +8,11 @@ from typing import Any
|
||||
from arcam.fmj import ConnectionFailed
|
||||
from arcam.fmj.client import Client
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
SIGNAL_CLIENT_DATA,
|
||||
SIGNAL_CLIENT_STARTED,
|
||||
SIGNAL_CLIENT_STOPPED,
|
||||
)
|
||||
|
||||
type ArcamFmjConfigEntry = ConfigEntry[Client]
|
||||
from .const import DEFAULT_SCAN_INTERVAL
|
||||
from .coordinator import ArcamFmjConfigEntry, ArcamFmjCoordinator, ArcamFmjRuntimeData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,24 +22,41 @@ PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) -> bool:
|
||||
"""Set up config entry."""
|
||||
entry.runtime_data = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
|
||||
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
|
||||
|
||||
coordinators: dict[int, ArcamFmjCoordinator] = {}
|
||||
for zone in (1, 2):
|
||||
coordinator = ArcamFmjCoordinator(hass, entry, client, zone)
|
||||
coordinators[zone] = coordinator
|
||||
|
||||
entry.runtime_data = ArcamFmjRuntimeData(client, coordinators)
|
||||
|
||||
entry.async_create_background_task(
|
||||
hass, _run_client(hass, entry.runtime_data, DEFAULT_SCAN_INTERVAL), "arcam_fmj"
|
||||
hass,
|
||||
_run_client(hass, entry.runtime_data, DEFAULT_SCAN_INTERVAL),
|
||||
"arcam_fmj",
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) -> bool:
|
||||
"""Cleanup before removing config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> None:
|
||||
async def _run_client(
|
||||
hass: HomeAssistant,
|
||||
runtime_data: ArcamFmjRuntimeData,
|
||||
interval: float,
|
||||
) -> None:
|
||||
client = runtime_data.client
|
||||
coordinators = runtime_data.coordinators
|
||||
|
||||
def _listen(_: Any) -> None:
|
||||
async_dispatcher_send(hass, SIGNAL_CLIENT_DATA, client.host)
|
||||
for coordinator in coordinators.values():
|
||||
coordinator.async_notify_data_updated()
|
||||
|
||||
while True:
|
||||
try:
|
||||
@@ -55,16 +64,21 @@ async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> N
|
||||
await client.start()
|
||||
|
||||
_LOGGER.debug("Client connected %s", client.host)
|
||||
async_dispatcher_send(hass, SIGNAL_CLIENT_STARTED, client.host)
|
||||
|
||||
try:
|
||||
for coordinator in coordinators.values():
|
||||
await coordinator.state.start()
|
||||
|
||||
with client.listen(_listen):
|
||||
for coordinator in coordinators.values():
|
||||
coordinator.async_notify_connected()
|
||||
await client.process()
|
||||
finally:
|
||||
await client.stop()
|
||||
|
||||
_LOGGER.debug("Client disconnected %s", client.host)
|
||||
async_dispatcher_send(hass, SIGNAL_CLIENT_STOPPED, client.host)
|
||||
for coordinator in coordinators.values():
|
||||
coordinator.async_notify_disconnected()
|
||||
|
||||
except ConnectionFailed:
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
DOMAIN = "arcam_fmj"
|
||||
|
||||
SIGNAL_CLIENT_STARTED = "arcam.client_started"
|
||||
SIGNAL_CLIENT_STOPPED = "arcam.client_stopped"
|
||||
SIGNAL_CLIENT_DATA = "arcam.client_data"
|
||||
|
||||
EVENT_TURN_ON = "arcam_fmj.turn_on"
|
||||
|
||||
DEFAULT_PORT = 50000
|
||||
|
||||
96
homeassistant/components/arcam_fmj/coordinator.py
Normal file
96
homeassistant/components/arcam_fmj/coordinator.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""Coordinator for Arcam FMJ integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from arcam.fmj import ConnectionFailed
|
||||
from arcam.fmj.client import Client
|
||||
from arcam.fmj.state import State
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArcamFmjRuntimeData:
|
||||
"""Runtime data for Arcam FMJ integration."""
|
||||
|
||||
client: Client
|
||||
coordinators: dict[int, ArcamFmjCoordinator]
|
||||
|
||||
|
||||
type ArcamFmjConfigEntry = ConfigEntry[ArcamFmjRuntimeData]
|
||||
|
||||
|
||||
class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Coordinator for a single Arcam FMJ zone."""
|
||||
|
||||
config_entry: ArcamFmjConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ArcamFmjConfigEntry,
|
||||
client: Client,
|
||||
zone: int,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"Arcam FMJ zone {zone}",
|
||||
)
|
||||
self.client = client
|
||||
self.state = State(client, zone)
|
||||
self.last_update_success = False
|
||||
|
||||
name = config_entry.title
|
||||
unique_id = config_entry.unique_id or config_entry.entry_id
|
||||
unique_id_device = unique_id
|
||||
if zone != 1:
|
||||
unique_id_device += f"-{zone}"
|
||||
name += f" Zone {zone}"
|
||||
|
||||
self.device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id_device)},
|
||||
manufacturer="Arcam",
|
||||
model="Arcam FMJ AVR",
|
||||
name=name,
|
||||
)
|
||||
|
||||
if zone != 1:
|
||||
self.device_info["via_device"] = (DOMAIN, unique_id)
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data for manual refresh."""
|
||||
try:
|
||||
await self.state.update()
|
||||
except ConnectionFailed as err:
|
||||
raise UpdateFailed(
|
||||
f"Connection failed during update for zone {self.state.zn}"
|
||||
) from err
|
||||
|
||||
@callback
|
||||
def async_notify_data_updated(self) -> None:
|
||||
"""Notify that new data has been received from the device."""
|
||||
self.async_set_updated_data(None)
|
||||
|
||||
@callback
|
||||
def async_notify_connected(self) -> None:
|
||||
"""Handle client connected."""
|
||||
self.hass.async_create_task(self.async_refresh())
|
||||
|
||||
@callback
|
||||
def async_notify_disconnected(self) -> None:
|
||||
"""Handle client disconnected."""
|
||||
self.last_update_success = False
|
||||
self.async_update_listeners()
|
||||
@@ -8,7 +8,6 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from arcam.fmj import ConnectionFailed, SourceCodes
|
||||
from arcam.fmj.state import State
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
BrowseError,
|
||||
@@ -20,20 +19,13 @@ from homeassistant.components.media_player import (
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import ArcamFmjConfigEntry
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
EVENT_TURN_ON,
|
||||
SIGNAL_CLIENT_DATA,
|
||||
SIGNAL_CLIENT_STARTED,
|
||||
SIGNAL_CLIENT_STOPPED,
|
||||
)
|
||||
from .const import EVENT_TURN_ON
|
||||
from .coordinator import ArcamFmjConfigEntry, ArcamFmjCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,19 +36,17 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the configuration entry."""
|
||||
|
||||
client = config_entry.runtime_data
|
||||
coordinators = config_entry.runtime_data.coordinators
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
ArcamFmj(
|
||||
config_entry.title,
|
||||
State(client, zone),
|
||||
coordinators[zone],
|
||||
config_entry.unique_id or config_entry.entry_id,
|
||||
)
|
||||
for zone in (1, 2)
|
||||
],
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
@@ -77,21 +67,21 @@ def convert_exception[**_P, _R](
|
||||
return _convert_exception
|
||||
|
||||
|
||||
class ArcamFmj(MediaPlayerEntity):
|
||||
class ArcamFmj(CoordinatorEntity[ArcamFmjCoordinator], MediaPlayerEntity):
|
||||
"""Representation of a media device."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_name: str,
|
||||
state: State,
|
||||
coordinator: ArcamFmjCoordinator,
|
||||
uuid: str,
|
||||
) -> None:
|
||||
"""Initialize device."""
|
||||
self._state = state
|
||||
self._attr_name = f"Zone {state.zn}"
|
||||
super().__init__(coordinator)
|
||||
self._state = coordinator.state
|
||||
self._attr_name = f"Zone {self._state.zn}"
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
@@ -102,18 +92,11 @@ class ArcamFmj(MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.TURN_ON
|
||||
)
|
||||
if state.zn == 1:
|
||||
if self._state.zn == 1:
|
||||
self._attr_supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||
self._attr_unique_id = f"{uuid}-{state.zn}"
|
||||
self._attr_entity_registry_enabled_default = state.zn == 1
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, uuid),
|
||||
},
|
||||
manufacturer="Arcam",
|
||||
model="Arcam FMJ AVR",
|
||||
name=device_name,
|
||||
)
|
||||
self._attr_unique_id = f"{uuid}-{self._state.zn}"
|
||||
self._attr_entity_registry_enabled_default = self._state.zn == 1
|
||||
self._attr_device_info = coordinator.device_info
|
||||
|
||||
@property
|
||||
def state(self) -> MediaPlayerState:
|
||||
@@ -122,49 +105,6 @@ class ArcamFmj(MediaPlayerEntity):
|
||||
return MediaPlayerState.ON
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Once registered, add listener for events."""
|
||||
await self._state.start()
|
||||
try:
|
||||
await self._state.update()
|
||||
except ConnectionFailed as connection:
|
||||
_LOGGER.debug("Connection lost during addition: %s", connection)
|
||||
|
||||
@callback
|
||||
def _data(host: str) -> None:
|
||||
if host == self._state.client.host:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _started(host: str) -> None:
|
||||
if host == self._state.client.host:
|
||||
self.async_schedule_update_ha_state(force_refresh=True)
|
||||
|
||||
@callback
|
||||
def _stopped(host: str) -> None:
|
||||
if host == self._state.client.host:
|
||||
self.async_schedule_update_ha_state(force_refresh=True)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, SIGNAL_CLIENT_DATA, _data)
|
||||
)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, SIGNAL_CLIENT_STARTED, _started)
|
||||
)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, SIGNAL_CLIENT_STOPPED, _stopped)
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Force update of state."""
|
||||
_LOGGER.debug("Update state %s", self.name)
|
||||
try:
|
||||
await self._state.update()
|
||||
except ConnectionFailed as connection:
|
||||
_LOGGER.debug("Connection lost during update: %s", connection)
|
||||
|
||||
@convert_exception
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Send mute command."""
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 2
|
||||
data:
|
||||
fields:
|
||||
access_token:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
client_secret:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 2
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
email:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
password:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,58 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
mode:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
password:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
protocol:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
ssh_key:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
username:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields:
|
||||
consider_home:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
dnsmasq:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
interface:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
require_ip:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
track_unknown:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
subentries: {}
|
||||
@@ -1,18 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
host:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
port:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -1,14 +0,0 @@
|
||||
config_entry:
|
||||
versions:
|
||||
- version:
|
||||
major: 1
|
||||
minor: 1
|
||||
data:
|
||||
fields:
|
||||
implementation:
|
||||
required: false
|
||||
selector:
|
||||
text: {}
|
||||
options:
|
||||
fields: {}
|
||||
subentries: {}
|
||||
@@ -30,5 +30,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.4"]
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.3.0"]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user