mirror of
https://github.com/home-assistant/core.git
synced 2026-04-29 10:23:46 +02:00
Compare commits
456 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87a00eb80f | |||
| 72efcf0e94 | |||
| 0459596e97 | |||
| 6ea59ffa94 | |||
| aab939cf6c | |||
| fe2402b611 | |||
| 029dbe7d94 | |||
| af6434a533 | |||
| dee4b33c64 | |||
| 8fff0075ba | |||
| d7caaceb64 | |||
| 3889482f0e | |||
| 1dc496a2dd | |||
| 056e6eae82 | |||
| 06e876aee0 | |||
| 713689491b | |||
| 17ab45da43 | |||
| 2a1df2063d | |||
| d88487e30b | |||
| 8884465262 | |||
| a85ccb94e3 | |||
| 391de22342 | |||
| 0592a39164 | |||
| dca287748d | |||
| 021878e942 | |||
| 0f2525d476 | |||
| 4d804649fc | |||
| 7209b3c7d3 | |||
| 6f88b6e64e | |||
| 4bcde36a97 | |||
| 7f4fc4d371 | |||
| 513361ef0f | |||
| 8ce236de80 | |||
| 8b8083a639 | |||
| 26ac8e35cb | |||
| 20600123f8 | |||
| b3d6f8861f | |||
| 5405279273 | |||
| 634582eab7 | |||
| 2c48f9aa4c | |||
| 8d0dda6523 | |||
| 6967c70580 | |||
| 7bab3579ec | |||
| aa8c4a6eb7 | |||
| 99a50fe874 | |||
| 65b48aa903 | |||
| af62e8267f | |||
| 45ab6e9b06 | |||
| a7a219b99b | |||
| c2d5696b5b | |||
| 2ef37f01b1 | |||
| 84def0c041 | |||
| 26ede9a679 | |||
| c0ee12ca41 | |||
| 926ffe536c | |||
| 2b2f5d6693 | |||
| d4f0aaa089 | |||
| 5b434aae6e | |||
| 1ffd797e0a | |||
| 8acc027f38 | |||
| 3139a7e431 | |||
| e7cb646a58 | |||
| ec9f50317f | |||
| fd0c63fe52 | |||
| 31aef86c0f | |||
| 943b96e7a1 | |||
| 74b78307ee | |||
| c0492d4af4 | |||
| f5b754a382 | |||
| cee695da28 | |||
| 2ef1c96325 | |||
| d3badb88ef | |||
| 1f80b803f7 | |||
| 2ea41c90b5 | |||
| 3fa24f87c0 | |||
| bfe19e82ff | |||
| 5108e1a1cd | |||
| 5e1b4b2d23 | |||
| 03a6eb26be | |||
| 0a11acf7ae | |||
| ab29718a45 | |||
| 7e7a6e4937 | |||
| c53c2d7e64 | |||
| 6e38cf878e | |||
| 066503b838 | |||
| 3e70342265 | |||
| 17994ff245 | |||
| cbd884d54a | |||
| b8c3a44d81 | |||
| ce28d8a92c | |||
| 457e66527a | |||
| b9bd8f6b34 | |||
| cd3059aa14 | |||
| 741add0666 | |||
| ee59303d3c | |||
| 20639b0f02 | |||
| ea7b2ecec0 | |||
| 09989e6184 | |||
| 883e33e72a | |||
| 49b07b3749 | |||
| 6b75c86a17 | |||
| f86bd3dfee | |||
| 069b7a45ed | |||
| e3e48ff9b7 | |||
| 33814d1180 | |||
| f126a6024e | |||
| b6d45a5a07 | |||
| f6c681eb5d | |||
| c4cc158a77 | |||
| e58cf00a96 | |||
| 6976a66758 | |||
| 6b1fc00910 | |||
| 8168b8fce4 | |||
| 1e6b6fef7e | |||
| 73f04e3ede | |||
| 9f469c08d1 | |||
| 051a28b55a | |||
| ff449e7741 | |||
| 8d239d368b | |||
| 3a5309e9a0 | |||
| 86ef7bab28 | |||
| f9928a5843 | |||
| 2c99f060f0 | |||
| 543f986955 | |||
| b68c90d59a | |||
| 66c6cd2a10 | |||
| f5f8c44ca6 | |||
| 58056c49f7 | |||
| af0a6d2820 | |||
| f3e2c51774 | |||
| 053e38db38 | |||
| ba81a68982 | |||
| f8c94fd83f | |||
| 9777ed2e62 | |||
| aba23eb513 | |||
| 0eda451c24 | |||
| 8f38b7191a | |||
| ff3cabbf3a | |||
| dfcfe78732 | |||
| ff20131af1 | |||
| ccbc300b68 | |||
| 84dcfb6ddc | |||
| 1db68327f9 | |||
| 7752789c3a | |||
| 54c15e7e0a | |||
| 0092796fd2 | |||
| 187a38c91f | |||
| b025942a14 | |||
| 0e515b2e1f | |||
| a341bfd8ca | |||
| f80acdada0 | |||
| cf049a07c2 | |||
| 0ca0836e83 | |||
| 60b0f0dc53 | |||
| c3921f2112 | |||
| aa619c5594 | |||
| 0677a256ec | |||
| 97ffbf5aad | |||
| 006b2da14e | |||
| 56b4ddc6b4 | |||
| 2c0c0b9e21 | |||
| 9e312f2063 | |||
| bbeecb40ae | |||
| 48c9361c01 | |||
| d686b877b1 | |||
| d2d01b337d | |||
| b0bfe71b9b | |||
| 38f3fa0210 | |||
| 86ae70780c | |||
| 65e16b4814 | |||
| 70966c2b63 | |||
| ba7f36328d | |||
| 511ecf98d5 | |||
| b5831344a0 | |||
| 984eba809c | |||
| f778033bd8 | |||
| a8f2204f4f | |||
| 4c56cbe8c8 | |||
| 71d35a03e1 | |||
| c8fd48523f | |||
| fbd3bf7a98 | |||
| a0356f587e | |||
| 199a4b725b | |||
| 505df84783 | |||
| baa9473383 | |||
| ba5d23290a | |||
| adda02b6b1 | |||
| 1f59bd9f92 | |||
| b61678d39c | |||
| b23297bb7e | |||
| f56c38d69b | |||
| c2b24dd355 | |||
| c4c8e74a8a | |||
| c4029300c2 | |||
| 52320844fc | |||
| b4e20409de | |||
| 4ecc6555bf | |||
| 892c32c8b7 | |||
| bad305dcbf | |||
| 7266a16295 | |||
| 416a2de179 | |||
| 349ea35dc3 | |||
| 0fb1fbf0d1 | |||
| 643fd34478 | |||
| 186c9aa33b | |||
| af51241c0d | |||
| eaee8d5b78 | |||
| 638434c103 | |||
| 3a44098ddf | |||
| 1e1c3506fe | |||
| b557e9e826 | |||
| da0d1b71ce | |||
| 4d96ed4c68 | |||
| 1bc63a61be | |||
| 5c35ccb9ca | |||
| a1ecefee21 | |||
| b5d7eba4f6 | |||
| 4b111008df | |||
| fb5afff9d5 | |||
| 38a1c97a51 | |||
| b26e4d672f | |||
| daa5268cf2 | |||
| 9da3f98c23 | |||
| 8fd691be69 | |||
| 7fc0e36b2f | |||
| 482bed522f | |||
| 7788685340 | |||
| af1af6f391 | |||
| d5c2e6ec35 | |||
| d8382c6de2 | |||
| c4cfff4b3f | |||
| cfe0c95c97 | |||
| 50c1bf8bb0 | |||
| cc3d059783 | |||
| 4aa86a574f | |||
| 3a8039cbc0 | |||
| e4f9f6447f | |||
| 14482ff6da | |||
| be8f14167f | |||
| 27032c1780 | |||
| 61a722218a | |||
| 3137c27e56 | |||
| 7b35c3036e | |||
| 8e03f3a045 | |||
| 8f26cff65a | |||
| 00533bae4b | |||
| 8255728f53 | |||
| 470335e27a | |||
| 56887747a6 | |||
| 1dcae0c0a6 | |||
| 8759a6a14d | |||
| 5d072d1030 | |||
| 78517f75e8 | |||
| 42ed7fbb0d | |||
| 96be3e2505 | |||
| 2fa3b9070c | |||
| d827c53a85 | |||
| 436ac72b82 | |||
| 7c15075231 | |||
| 8e3ad2d1f3 | |||
| 733bbf9cd1 | |||
| 822660732b | |||
| d6bd4312ab | |||
| 491bde181c | |||
| fdce524811 | |||
| cf10549df4 | |||
| fd01e22ca4 | |||
| 334359bb0a | |||
| eda1656e75 | |||
| 6cea6be4a7 | |||
| 6ecc5c19a2 | |||
| c321bd70e1 | |||
| 851600630c | |||
| e3896d1f60 | |||
| c71cf272c8 | |||
| d12c6f89d2 | |||
| 5965d8d503 | |||
| 94f458ff98 | |||
| c07a9e9d59 | |||
| b9db9eeab2 | |||
| f34b449f61 | |||
| fc24843274 | |||
| 22b6239304 | |||
| aa8fe99113 | |||
| 7c223db1d5 | |||
| 0c18b2e7ff | |||
| d68ee8dcea | |||
| 671aaa7e95 | |||
| faefe624f6 | |||
| f93259a2f1 | |||
| 606524f9e7 | |||
| cd89db9bb6 | |||
| f760c13e8f | |||
| 687cd32142 | |||
| fb27297df9 | |||
| 3e350bdc90 | |||
| 0b14f0a379 | |||
| 3206979488 | |||
| 4c27bfbf7f | |||
| 7c4fd9473c | |||
| 633c904852 | |||
| 5300eddf33 | |||
| 9f558d13e6 | |||
| 9ae59e5ea0 | |||
| 1b1c1c2a55 | |||
| df4bd721b5 | |||
| baa876d4d9 | |||
| b99dceab74 | |||
| 114e254aa6 | |||
| fbfd8c48aa | |||
| d40e3145fe | |||
| f4a16c8dc9 | |||
| 2ce6bd2378 | |||
| 9334099bed | |||
| 077edb08f6 | |||
| 72d5146a3e | |||
| fa14321aa1 | |||
| 8f679fcbf3 | |||
| 78cf7dc873 | |||
| 9fff3a13a5 | |||
| 99f43400bf | |||
| 77b464f2bd | |||
| 07e251d488 | |||
| 659d135fca | |||
| 24414369d7 | |||
| 92c1fb77e9 | |||
| ae1f53775f | |||
| bd6b5568eb | |||
| 5f2964d3e8 | |||
| c6865d0862 | |||
| ef8fc3913e | |||
| 56667ec2bc | |||
| fa21613951 | |||
| f735d12a66 | |||
| 2f7a396778 | |||
| 12336f5c15 | |||
| 1661304f10 | |||
| 68162e1a27 | |||
| 95a25c72dc | |||
| 30772da0e1 | |||
| ef84a8869e | |||
| d3879a36d1 | |||
| 93afc9458a | |||
| 5cd8e4ab7e | |||
| 994c2ebca1 | |||
| 81f5068354 | |||
| 3e60d7aa11 | |||
| 30aa3a26ad | |||
| 9da5dd0090 | |||
| 65f007ace7 | |||
| 2a8feda691 | |||
| 36b7e8569e | |||
| 221f961574 | |||
| 7210cc1da6 | |||
| 5fa23b1785 | |||
| 2cab9f7fe9 | |||
| 3bfcb1ebdd | |||
| c1eb5f8b74 | |||
| 582b7eab66 | |||
| 26281662b5 | |||
| ac39bf991f | |||
| 0a9e20615e | |||
| 8cafa1bcdf | |||
| 66ddf44399 | |||
| 933ae143b3 | |||
| 8c2e63807c | |||
| 460363c4ba | |||
| 29a17edaa5 | |||
| ed161d3d49 | |||
| 7868ffac35 | |||
| 910fb0930e | |||
| cb742a677c | |||
| 28c24e5fef | |||
| 50577883dc | |||
| 20f9b9e412 | |||
| 1d05a917f9 | |||
| 240bd6c3bf | |||
| c01bb44757 | |||
| 5e93394ae7 | |||
| a8b55a16fd | |||
| d7fb245213 | |||
| 6467c8d611 | |||
| c47b37af4f | |||
| a5bacf5652 | |||
| 6589216ed3 | |||
| b6dc410464 | |||
| 928ff7c78c | |||
| c9335598db | |||
| 32babd3958 | |||
| 7f405686d1 | |||
| 5bd736029f | |||
| 397198c6d0 | |||
| 54188b4128 | |||
| f3da9de744 | |||
| aeb95c4509 | |||
| f394dfb8d0 | |||
| 6781a76de2 | |||
| 69a9aa4594 | |||
| afa02dcce9 | |||
| febb382030 | |||
| 1906155c18 | |||
| ffabd5d7db | |||
| 9e2360791d | |||
| 19cbc1b258 | |||
| df2ea1e875 | |||
| 74fa30e59d | |||
| 6833af6286 | |||
| 4940968cd5 | |||
| a9975071c3 | |||
| cc4340b80c | |||
| 252f05e0f7 | |||
| f5e0382123 | |||
| 600c6a0dcb | |||
| df60e59a95 | |||
| cf90e77e57 | |||
| 3e0bd44d2a | |||
| 7bb93d4f3e | |||
| 4dfc11a140 | |||
| 7eeebf198b | |||
| 175ffe29f6 | |||
| ff9937f942 | |||
| ef452427e3 | |||
| a04970bd54 | |||
| 149aebb0bc | |||
| c36fc70ab4 | |||
| 681fe3485d | |||
| 34680becaa | |||
| 354f4491c8 | |||
| c4fd1cfc8f | |||
| a4e9e4b23b | |||
| eac7794741 | |||
| 1cb9690001 | |||
| 1101e7ef64 | |||
| 3b6128d590 | |||
| 7f4fca63ed | |||
| 4b59ef4733 | |||
| 3d39f6ce88 | |||
| 5f810d908f | |||
| c7cfd56b72 | |||
| ada6b7875c | |||
| 2b20b2a80b | |||
| 5825e8fee8 | |||
| 70488ffd15 | |||
| 2900fa733d | |||
| 7d61dd13d9 | |||
| af8131e68f | |||
| c049129147 | |||
| 2dce876a86 | |||
| 148bb05dea | |||
| 4d01e0a773 | |||
| 15d8b84074 | |||
| 2208262ca5 | |||
| 0c2a0118e2 | |||
| 93fe46509f | |||
| 581efad5a7 | |||
| 0492639d51 |
@@ -14,6 +14,7 @@ core: &core
|
||||
base_platforms: &base_platforms
|
||||
- homeassistant/components/air_quality/**
|
||||
- homeassistant/components/alarm_control_panel/**
|
||||
- homeassistant/components/assist_satellite/**
|
||||
- homeassistant/components/binary_sensor/**
|
||||
- homeassistant/components/button/**
|
||||
- homeassistant/components/calendar/**
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -453,7 +453,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
||||
+49
-34
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 10
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 8
|
||||
HA_SHORT_VERSION: "2024.9"
|
||||
HA_SHORT_VERSION: "2024.10"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -234,7 +234,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -279,7 +279,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -319,7 +319,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -359,7 +359,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -429,17 +429,32 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files
|
||||
|
||||
lint-hadolint:
|
||||
name: Check ${{ matrix.file }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- info
|
||||
if: |
|
||||
github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
file:
|
||||
- Dockerfile
|
||||
- Dockerfile.dev
|
||||
- script/hassfest/docker/Dockerfile
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Register hadolint problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||
- name: Check Dockerfile
|
||||
uses: docker://hadolint/hadolint:v1.18.2
|
||||
- name: Check ${{ matrix.file }}
|
||||
uses: docker://hadolint/hadolint:v2.12.0
|
||||
with:
|
||||
args: hadolint Dockerfile
|
||||
- name: Check Dockerfile.dev
|
||||
uses: docker://hadolint/hadolint:v1.18.2
|
||||
with:
|
||||
args: hadolint Dockerfile.dev
|
||||
args: hadolint ${{ matrix.file }}
|
||||
|
||||
base:
|
||||
name: Prepare dependencies
|
||||
@@ -454,7 +469,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -538,7 +553,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -571,7 +586,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -605,7 +620,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -623,7 +638,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pip-licenses --format=json --output-file=licenses.json
|
||||
- name: Upload licenses
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: licenses
|
||||
path: licenses.json
|
||||
@@ -648,7 +663,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -695,7 +710,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -740,7 +755,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -815,7 +830,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -833,7 +848,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
||||
- name: Upload pytest_buckets
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: pytest_buckets
|
||||
path: pytest_buckets.txt
|
||||
@@ -879,7 +894,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -934,14 +949,14 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.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@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
@@ -999,7 +1014,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1060,7 +1075,7 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -1068,7 +1083,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -1125,7 +1140,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1187,7 +1202,7 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1195,7 +1210,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1271,7 +1286,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1329,14 +1344,14 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.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@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
|
||||
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.26.5
|
||||
uses: github/codeql-action/init@v3.26.6
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.26.5
|
||||
uses: github/codeql-action/analyze@v3.26.6
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -82,14 +82,15 @@ jobs:
|
||||
) > .env_file
|
||||
|
||||
- name: Upload env_file
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
include-hidden-files: true
|
||||
overwrite: true
|
||||
|
||||
- name: Upload requirements_diff
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
path: ./requirements_diff.txt
|
||||
@@ -101,7 +102,7 @@ jobs:
|
||||
python -m script.gen_requirements_all ci
|
||||
|
||||
- name: Upload requirements_all_wheels
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
path: ./requirements_all_wheels_*.txt
|
||||
@@ -139,7 +140,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "libffi-dev;openssl-dev;yaml-dev;nasm"
|
||||
skip-binary: aiohttp
|
||||
skip-binary: aiohttp;multidict;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements.txt"
|
||||
@@ -211,7 +212,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_old-cython.txt"
|
||||
@@ -226,7 +227,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtaa"
|
||||
@@ -240,7 +241,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtab"
|
||||
@@ -254,7 +255,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtac"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.2
|
||||
rev: v0.6.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
||||
@@ -95,6 +95,7 @@ homeassistant.components.aruba.*
|
||||
homeassistant.components.arwn.*
|
||||
homeassistant.components.aseko_pool_live.*
|
||||
homeassistant.components.assist_pipeline.*
|
||||
homeassistant.components.assist_satellite.*
|
||||
homeassistant.components.asuswrt.*
|
||||
homeassistant.components.autarco.*
|
||||
homeassistant.components.auth.*
|
||||
@@ -110,6 +111,7 @@ homeassistant.components.bitcoin.*
|
||||
homeassistant.components.blockchain.*
|
||||
homeassistant.components.blue_current.*
|
||||
homeassistant.components.blueprint.*
|
||||
homeassistant.components.bluesound.*
|
||||
homeassistant.components.bluetooth.*
|
||||
homeassistant.components.bluetooth_adapters.*
|
||||
homeassistant.components.bluetooth_tracker.*
|
||||
@@ -139,6 +141,7 @@ homeassistant.components.cpuspeed.*
|
||||
homeassistant.components.crownstone.*
|
||||
homeassistant.components.date.*
|
||||
homeassistant.components.datetime.*
|
||||
homeassistant.components.deako.*
|
||||
homeassistant.components.deconz.*
|
||||
homeassistant.components.default_config.*
|
||||
homeassistant.components.demo.*
|
||||
@@ -208,6 +211,8 @@ homeassistant.components.glances.*
|
||||
homeassistant.components.goalzero.*
|
||||
homeassistant.components.google.*
|
||||
homeassistant.components.google_assistant_sdk.*
|
||||
homeassistant.components.google_cloud.*
|
||||
homeassistant.components.google_photos.*
|
||||
homeassistant.components.google_sheets.*
|
||||
homeassistant.components.gpsd.*
|
||||
homeassistant.components.greeneye_monitor.*
|
||||
@@ -278,6 +283,7 @@ homeassistant.components.lawn_mower.*
|
||||
homeassistant.components.lcn.*
|
||||
homeassistant.components.ld2410_ble.*
|
||||
homeassistant.components.led_ble.*
|
||||
homeassistant.components.lektrico.*
|
||||
homeassistant.components.lidarr.*
|
||||
homeassistant.components.lifx.*
|
||||
homeassistant.components.light.*
|
||||
@@ -310,6 +316,7 @@ homeassistant.components.minecraft_server.*
|
||||
homeassistant.components.mjpeg.*
|
||||
homeassistant.components.modbus.*
|
||||
homeassistant.components.modem_callerid.*
|
||||
homeassistant.components.mold_indicator.*
|
||||
homeassistant.components.monzo.*
|
||||
homeassistant.components.moon.*
|
||||
homeassistant.components.mopeka.*
|
||||
@@ -336,6 +343,7 @@ homeassistant.components.nut.*
|
||||
homeassistant.components.onboarding.*
|
||||
homeassistant.components.oncue.*
|
||||
homeassistant.components.onewire.*
|
||||
homeassistant.components.onkyo.*
|
||||
homeassistant.components.open_meteo.*
|
||||
homeassistant.components.openexchangerates.*
|
||||
homeassistant.components.opensky.*
|
||||
@@ -395,6 +403,7 @@ homeassistant.components.select.*
|
||||
homeassistant.components.sensibo.*
|
||||
homeassistant.components.sensirion_ble.*
|
||||
homeassistant.components.sensor.*
|
||||
homeassistant.components.sensoterra.*
|
||||
homeassistant.components.senz.*
|
||||
homeassistant.components.sfr_box.*
|
||||
homeassistant.components.shelly.*
|
||||
@@ -407,9 +416,11 @@ homeassistant.components.slack.*
|
||||
homeassistant.components.sleepiq.*
|
||||
homeassistant.components.smhi.*
|
||||
homeassistant.components.snooz.*
|
||||
homeassistant.components.solarlog.*
|
||||
homeassistant.components.sonarr.*
|
||||
homeassistant.components.speedtestdotnet.*
|
||||
homeassistant.components.sql.*
|
||||
homeassistant.components.squeezebox.*
|
||||
homeassistant.components.ssdp.*
|
||||
homeassistant.components.starlink.*
|
||||
homeassistant.components.statistics.*
|
||||
|
||||
+24
-5
@@ -143,6 +143,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/aseko_pool_live/ @milanmeu
|
||||
/homeassistant/components/assist_pipeline/ @balloob @synesthesiam
|
||||
/tests/components/assist_pipeline/ @balloob @synesthesiam
|
||||
/homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam
|
||||
/tests/components/assist_satellite/ @home-assistant/core @synesthesiam
|
||||
/homeassistant/components/asuswrt/ @kennedyshead @ollo69
|
||||
/tests/components/asuswrt/ @kennedyshead @ollo69
|
||||
/homeassistant/components/atag/ @MatsNL
|
||||
@@ -228,8 +230,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/bsblan/ @liudger
|
||||
/tests/components/bsblan/ @liudger
|
||||
/homeassistant/components/bt_smarthub/ @typhoon2099
|
||||
/homeassistant/components/bthome/ @Ernst79
|
||||
/tests/components/bthome/ @Ernst79
|
||||
/homeassistant/components/bthome/ @Ernst79 @thecode
|
||||
/tests/components/bthome/ @Ernst79 @thecode
|
||||
/homeassistant/components/buienradar/ @mjj4791 @ties @Robbie1221
|
||||
/tests/components/buienradar/ @mjj4791 @ties @Robbie1221
|
||||
/homeassistant/components/button/ @home-assistant/core
|
||||
@@ -294,6 +296,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/date/ @home-assistant/core
|
||||
/homeassistant/components/datetime/ @home-assistant/core
|
||||
/tests/components/datetime/ @home-assistant/core
|
||||
/homeassistant/components/deako/ @sebirdman @balake @deakolights
|
||||
/tests/components/deako/ @sebirdman @balake @deakolights
|
||||
/homeassistant/components/debugpy/ @frenck
|
||||
/tests/components/debugpy/ @frenck
|
||||
/homeassistant/components/deconz/ @Kane610
|
||||
@@ -547,11 +551,14 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/google_assistant/ @home-assistant/cloud
|
||||
/homeassistant/components/google_assistant_sdk/ @tronikos
|
||||
/tests/components/google_assistant_sdk/ @tronikos
|
||||
/homeassistant/components/google_cloud/ @lufton
|
||||
/homeassistant/components/google_cloud/ @lufton @tronikos
|
||||
/tests/components/google_cloud/ @lufton @tronikos
|
||||
/homeassistant/components/google_generative_ai_conversation/ @tronikos
|
||||
/tests/components/google_generative_ai_conversation/ @tronikos
|
||||
/homeassistant/components/google_mail/ @tkdrob
|
||||
/tests/components/google_mail/ @tkdrob
|
||||
/homeassistant/components/google_photos/ @allenporter
|
||||
/tests/components/google_photos/ @allenporter
|
||||
/homeassistant/components/google_sheets/ @tkdrob
|
||||
/tests/components/google_sheets/ @tkdrob
|
||||
/homeassistant/components/google_tasks/ @allenporter
|
||||
@@ -629,6 +636,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/homewizard/ @DCSBL
|
||||
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
||||
/tests/components/honeywell/ @rdfurman @mkmer
|
||||
/homeassistant/components/html5/ @alexyao2015
|
||||
/tests/components/html5/ @alexyao2015
|
||||
/homeassistant/components/http/ @home-assistant/core
|
||||
/tests/components/http/ @home-assistant/core
|
||||
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
||||
@@ -707,8 +716,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ios/ @robbiet480
|
||||
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
||||
/tests/components/iotawatt/ @gtdiehl @jyavenard
|
||||
/homeassistant/components/iotty/ @pburgio
|
||||
/tests/components/iotty/ @pburgio
|
||||
/homeassistant/components/iotty/ @pburgio @shapournemati-iotty
|
||||
/tests/components/iotty/ @pburgio @shapournemati-iotty
|
||||
/homeassistant/components/iperf3/ @rohankapoorcom
|
||||
/homeassistant/components/ipma/ @dgomes
|
||||
/tests/components/ipma/ @dgomes
|
||||
@@ -721,6 +730,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/iron_os/ @tr4nt0r
|
||||
/homeassistant/components/isal/ @bdraco
|
||||
/tests/components/isal/ @bdraco
|
||||
/homeassistant/components/iskra/ @iskramis
|
||||
/tests/components/iskra/ @iskramis
|
||||
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||
/homeassistant/components/israel_rail/ @shaiu
|
||||
@@ -797,8 +808,12 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/leaone/ @bdraco
|
||||
/homeassistant/components/led_ble/ @bdraco
|
||||
/tests/components/led_ble/ @bdraco
|
||||
/homeassistant/components/lektrico/ @lektrico
|
||||
/tests/components/lektrico/ @lektrico
|
||||
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
||||
/homeassistant/components/lidarr/ @tkdrob
|
||||
/tests/components/lidarr/ @tkdrob
|
||||
/homeassistant/components/lifx/ @Djelibeybi
|
||||
@@ -1275,6 +1290,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/sensorpro/ @bdraco
|
||||
/homeassistant/components/sensorpush/ @bdraco
|
||||
/tests/components/sensorpush/ @bdraco
|
||||
/homeassistant/components/sensoterra/ @markruys
|
||||
/tests/components/sensoterra/ @markruys
|
||||
/homeassistant/components/sentry/ @dcramer @frenck
|
||||
/tests/components/sentry/ @dcramer @frenck
|
||||
/homeassistant/components/senz/ @milanmeu
|
||||
@@ -1623,6 +1640,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/webostv/ @thecode
|
||||
/homeassistant/components/websocket_api/ @home-assistant/core
|
||||
/tests/components/websocket_api/ @home-assistant/core
|
||||
/homeassistant/components/weheat/ @jesperraemaekers
|
||||
/tests/components/weheat/ @jesperraemaekers
|
||||
/homeassistant/components/wemo/ @esev
|
||||
/tests/components/wemo/ @esev
|
||||
/homeassistant/components/whirlpool/ @abmantis @mkmer
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"google_generative_ai_conversation",
|
||||
"google_mail",
|
||||
"google_maps",
|
||||
"google_photos",
|
||||
"google_pubsub",
|
||||
"google_sheets",
|
||||
"google_tasks",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "lg",
|
||||
"name": "LG",
|
||||
"integrations": ["lg_netcast", "lg_soundbar", "webostv"]
|
||||
"integrations": ["lg_netcast", "lg_thinq", "lg_soundbar", "webostv"]
|
||||
}
|
||||
|
||||
@@ -6,52 +6,3 @@ Component design guidelines:
|
||||
format "<DOMAIN>.<OBJECT_ID>".
|
||||
- Each component should publish services only under its own domain.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.helpers.frame import report
|
||||
from homeassistant.helpers.group import expand_entity_ids
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool:
|
||||
"""Load up the module to call the is_on method.
|
||||
|
||||
If there is no entity id given we will check all.
|
||||
"""
|
||||
report(
|
||||
(
|
||||
"uses homeassistant.components.is_on."
|
||||
" This is deprecated and will stop working in Home Assistant 2024.9, it"
|
||||
" should be updated to use the function of the platform directly."
|
||||
),
|
||||
error_if_core=True,
|
||||
)
|
||||
|
||||
if entity_id:
|
||||
entity_ids = expand_entity_ids(hass, [entity_id])
|
||||
else:
|
||||
entity_ids = hass.states.entity_ids()
|
||||
|
||||
for ent_id in entity_ids:
|
||||
domain = split_entity_id(ent_id)[0]
|
||||
|
||||
try:
|
||||
component = getattr(hass.components, domain)
|
||||
|
||||
except ImportError:
|
||||
_LOGGER.error("Failed to call %s.is_on: component not found", domain)
|
||||
continue
|
||||
|
||||
if not hasattr(component, "is_on"):
|
||||
_LOGGER.warning("Integration %s has no is_on method", domain)
|
||||
continue
|
||||
|
||||
if component.is_on(ent_id):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||
UV_INDEX,
|
||||
UnitOfIrradiance,
|
||||
UnitOfLength,
|
||||
UnitOfPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
@@ -279,6 +280,15 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||
translation_key="realfeel_temperature_shade",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="RelativeHumidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda data: cast(int, data),
|
||||
translation_key="humidity",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="Precipitation",
|
||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||
@@ -288,6 +298,16 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
attr_fn=lambda data: {"type": data["PrecipitationType"]},
|
||||
translation_key="precipitation",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="Pressure",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||
translation_key="pressure",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="PressureTendency",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
@@ -295,9 +315,19 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
value_fn=lambda data: cast(str, data["LocalizedText"]).lower(),
|
||||
translation_key="pressure_tendency",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="Temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||
translation_key="temperature",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="UVIndex",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UV_INDEX,
|
||||
value_fn=lambda data: cast(int, data),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]},
|
||||
@@ -324,6 +354,7 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
AccuWeatherSensorDescription(
|
||||
key="Wind",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from .hub import PulseHub
|
||||
|
||||
@@ -17,6 +18,9 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: AcmedaConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Rollease Acmeda Automate hub from a config entry."""
|
||||
|
||||
await _migrate_unique_ids(hass, config_entry)
|
||||
|
||||
hub = PulseHub(hass, config_entry)
|
||||
|
||||
if not await hub.async_setup():
|
||||
@@ -28,6 +32,19 @@ async def async_setup_entry(
|
||||
return True
|
||||
|
||||
|
||||
async def _migrate_unique_ids(hass: HomeAssistant, entry: AcmedaConfigEntry) -> None:
|
||||
"""Migrate pre-config flow unique ids."""
|
||||
entity_registry = er.async_get(hass)
|
||||
registry_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, entry.entry_id
|
||||
)
|
||||
for reg_entry in registry_entries:
|
||||
if isinstance(reg_entry.unique_id, int): # type: ignore[unreachable]
|
||||
entity_registry.async_update_entity( # type: ignore[unreachable]
|
||||
reg_entry.entity_id, new_unique_id=str(reg_entry.unique_id)
|
||||
)
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: AcmedaConfigEntry
|
||||
) -> bool:
|
||||
|
||||
@@ -67,7 +67,7 @@ class AcmedaBase(entity.Entity):
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID of this roller."""
|
||||
return self.roller.id # type: ignore[no-any-return]
|
||||
return str(self.roller.id)
|
||||
|
||||
@property
|
||||
def device_id(self) -> str:
|
||||
|
||||
@@ -29,18 +29,40 @@ DATA_ADS = "data_ads"
|
||||
# Supported Types
|
||||
ADSTYPE_BOOL = "bool"
|
||||
ADSTYPE_BYTE = "byte"
|
||||
ADSTYPE_DINT = "dint"
|
||||
ADSTYPE_INT = "int"
|
||||
ADSTYPE_UDINT = "udint"
|
||||
ADSTYPE_UINT = "uint"
|
||||
ADSTYPE_SINT = "sint"
|
||||
ADSTYPE_USINT = "usint"
|
||||
ADSTYPE_DINT = "dint"
|
||||
ADSTYPE_UDINT = "udint"
|
||||
ADSTYPE_WORD = "word"
|
||||
ADSTYPE_DWORD = "dword"
|
||||
ADSTYPE_LREAL = "lreal"
|
||||
ADSTYPE_REAL = "real"
|
||||
ADSTYPE_STRING = "string"
|
||||
ADSTYPE_TIME = "time"
|
||||
ADSTYPE_DATE = "date"
|
||||
ADSTYPE_DATE_AND_TIME = "dt"
|
||||
ADSTYPE_TOD = "tod"
|
||||
|
||||
ADS_TYPEMAP = {
|
||||
ADSTYPE_BOOL: pyads.PLCTYPE_BOOL,
|
||||
ADSTYPE_BYTE: pyads.PLCTYPE_BYTE,
|
||||
ADSTYPE_DINT: pyads.PLCTYPE_DINT,
|
||||
ADSTYPE_INT: pyads.PLCTYPE_INT,
|
||||
ADSTYPE_UDINT: pyads.PLCTYPE_UDINT,
|
||||
ADSTYPE_UINT: pyads.PLCTYPE_UINT,
|
||||
ADSTYPE_SINT: pyads.PLCTYPE_SINT,
|
||||
ADSTYPE_USINT: pyads.PLCTYPE_USINT,
|
||||
ADSTYPE_DINT: pyads.PLCTYPE_DINT,
|
||||
ADSTYPE_UDINT: pyads.PLCTYPE_UDINT,
|
||||
ADSTYPE_WORD: pyads.PLCTYPE_WORD,
|
||||
ADSTYPE_DWORD: pyads.PLCTYPE_DWORD,
|
||||
ADSTYPE_REAL: pyads.PLCTYPE_REAL,
|
||||
ADSTYPE_LREAL: pyads.PLCTYPE_LREAL,
|
||||
ADSTYPE_STRING: pyads.PLCTYPE_STRING,
|
||||
ADSTYPE_TIME: pyads.PLCTYPE_TIME,
|
||||
ADSTYPE_DATE: pyads.PLCTYPE_DATE,
|
||||
ADSTYPE_DATE_AND_TIME: pyads.PLCTYPE_DT,
|
||||
ADSTYPE_TOD: pyads.PLCTYPE_TOD,
|
||||
}
|
||||
|
||||
CONF_ADS_FACTOR = "factor"
|
||||
@@ -75,12 +97,23 @@ SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ADS_TYPE): vol.In(
|
||||
[
|
||||
ADSTYPE_BOOL,
|
||||
ADSTYPE_BYTE,
|
||||
ADSTYPE_INT,
|
||||
ADSTYPE_UINT,
|
||||
ADSTYPE_BYTE,
|
||||
ADSTYPE_BOOL,
|
||||
ADSTYPE_SINT,
|
||||
ADSTYPE_USINT,
|
||||
ADSTYPE_DINT,
|
||||
ADSTYPE_UDINT,
|
||||
ADSTYPE_WORD,
|
||||
ADSTYPE_DWORD,
|
||||
ADSTYPE_REAL,
|
||||
ADSTYPE_LREAL,
|
||||
ADSTYPE_STRING,
|
||||
ADSTYPE_TIME,
|
||||
ADSTYPE_DATE,
|
||||
ADSTYPE_DATE_AND_TIME,
|
||||
ADSTYPE_TOD,
|
||||
]
|
||||
),
|
||||
vol.Required(CONF_ADS_VALUE): vol.Coerce(int),
|
||||
@@ -222,37 +255,53 @@ class AdsHub:
|
||||
def _device_notification_callback(self, notification, name):
|
||||
"""Handle device notifications."""
|
||||
contents = notification.contents
|
||||
|
||||
hnotify = int(contents.hNotification)
|
||||
_LOGGER.debug("Received notification %d", hnotify)
|
||||
|
||||
# get dynamically sized data array
|
||||
# Get dynamically sized data array
|
||||
data_size = contents.cbSampleSize
|
||||
data = (ctypes.c_ubyte * data_size).from_address(
|
||||
data_address = (
|
||||
ctypes.addressof(contents)
|
||||
+ pyads.structs.SAdsNotificationHeader.data.offset
|
||||
)
|
||||
data = (ctypes.c_ubyte * data_size).from_address(data_address)
|
||||
|
||||
try:
|
||||
with self._lock:
|
||||
notification_item = self._notification_items[hnotify]
|
||||
except KeyError:
|
||||
# Acquire notification item
|
||||
with self._lock:
|
||||
notification_item = self._notification_items.get(hnotify)
|
||||
|
||||
if not notification_item:
|
||||
_LOGGER.error("Unknown device notification handle: %d", hnotify)
|
||||
return
|
||||
|
||||
# Parse data to desired datatype
|
||||
if notification_item.plc_datatype == pyads.PLCTYPE_BOOL:
|
||||
# Data parsing based on PLC data type
|
||||
plc_datatype = notification_item.plc_datatype
|
||||
unpack_formats = {
|
||||
pyads.PLCTYPE_BYTE: "<b",
|
||||
pyads.PLCTYPE_INT: "<h",
|
||||
pyads.PLCTYPE_UINT: "<H",
|
||||
pyads.PLCTYPE_SINT: "<b",
|
||||
pyads.PLCTYPE_USINT: "<B",
|
||||
pyads.PLCTYPE_DINT: "<i",
|
||||
pyads.PLCTYPE_UDINT: "<I",
|
||||
pyads.PLCTYPE_WORD: "<H",
|
||||
pyads.PLCTYPE_DWORD: "<I",
|
||||
pyads.PLCTYPE_LREAL: "<d",
|
||||
pyads.PLCTYPE_REAL: "<f",
|
||||
pyads.PLCTYPE_TOD: "<i", # Treat as DINT
|
||||
pyads.PLCTYPE_DATE: "<i", # Treat as DINT
|
||||
pyads.PLCTYPE_DT: "<i", # Treat as DINT
|
||||
pyads.PLCTYPE_TIME: "<i", # Treat as DINT
|
||||
}
|
||||
|
||||
if plc_datatype == pyads.PLCTYPE_BOOL:
|
||||
value = bool(struct.unpack("<?", bytearray(data))[0])
|
||||
elif notification_item.plc_datatype == pyads.PLCTYPE_INT:
|
||||
value = struct.unpack("<h", bytearray(data))[0]
|
||||
elif notification_item.plc_datatype == pyads.PLCTYPE_BYTE:
|
||||
value = struct.unpack("<B", bytearray(data))[0]
|
||||
elif notification_item.plc_datatype == pyads.PLCTYPE_UINT:
|
||||
value = struct.unpack("<H", bytearray(data))[0]
|
||||
elif notification_item.plc_datatype == pyads.PLCTYPE_DINT:
|
||||
value = struct.unpack("<i", bytearray(data))[0]
|
||||
elif notification_item.plc_datatype == pyads.PLCTYPE_UDINT:
|
||||
value = struct.unpack("<I", bytearray(data))[0]
|
||||
elif plc_datatype == pyads.PLCTYPE_STRING:
|
||||
value = (
|
||||
bytearray(data).split(b"\x00", 1)[0].decode("utf-8", errors="ignore")
|
||||
)
|
||||
elif plc_datatype in unpack_formats:
|
||||
value = struct.unpack(unpack_formats[plc_datatype], bytearray(data))[0]
|
||||
else:
|
||||
value = bytearray(data)
|
||||
_LOGGER.warning("No callback available for this datatype")
|
||||
|
||||
@@ -2,18 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from airgradient import AirGradientClient, get_model_name
|
||||
from airgradient import AirGradientClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
|
||||
from .coordinator import AirGradientCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BUTTON,
|
||||
@@ -25,15 +21,7 @@ PLATFORMS: list[Platform] = [
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AirGradientData:
|
||||
"""AirGradient data class."""
|
||||
|
||||
measurement: AirGradientMeasurementCoordinator
|
||||
config: AirGradientConfigCoordinator
|
||||
|
||||
|
||||
type AirGradientConfigEntry = ConfigEntry[AirGradientData]
|
||||
type AirGradientConfigEntry = ConfigEntry[AirGradientCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool:
|
||||
@@ -43,27 +31,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry)
|
||||
entry.data[CONF_HOST], session=async_get_clientsession(hass)
|
||||
)
|
||||
|
||||
measurement_coordinator = AirGradientMeasurementCoordinator(hass, client)
|
||||
config_coordinator = AirGradientConfigCoordinator(hass, client)
|
||||
coordinator = AirGradientCoordinator(hass, client)
|
||||
|
||||
await measurement_coordinator.async_config_entry_first_refresh()
|
||||
await config_coordinator.async_config_entry_first_refresh()
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, measurement_coordinator.serial_number)},
|
||||
manufacturer="AirGradient",
|
||||
model=get_model_name(measurement_coordinator.data.model),
|
||||
model_id=measurement_coordinator.data.model,
|
||||
serial_number=measurement_coordinator.data.serial_number,
|
||||
sw_version=measurement_coordinator.data.firmware_version,
|
||||
)
|
||||
|
||||
entry.runtime_data = AirGradientData(
|
||||
measurement=measurement_coordinator,
|
||||
config=config_coordinator,
|
||||
)
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import DOMAIN, AirGradientConfigEntry
|
||||
from .coordinator import AirGradientConfigCoordinator
|
||||
from . import AirGradientConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirGradientCoordinator
|
||||
from .entity import AirGradientEntity
|
||||
|
||||
|
||||
@@ -47,8 +48,8 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AirGradient button entities based on a config entry."""
|
||||
model = entry.runtime_data.measurement.data.model
|
||||
coordinator = entry.runtime_data.config
|
||||
coordinator = entry.runtime_data
|
||||
model = coordinator.data.measures.model
|
||||
|
||||
added_entities = False
|
||||
|
||||
@@ -57,7 +58,7 @@ async def async_setup_entry(
|
||||
nonlocal added_entities
|
||||
|
||||
if (
|
||||
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||
coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
|
||||
and not added_entities
|
||||
):
|
||||
entities = [AirGradientButton(coordinator, CO2_CALIBRATION)]
|
||||
@@ -67,7 +68,8 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
added_entities = True
|
||||
elif (
|
||||
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||
coordinator.data.config.configuration_control
|
||||
is not ConfigurationControl.LOCAL
|
||||
and added_entities
|
||||
):
|
||||
entity_registry = er.async_get(hass)
|
||||
@@ -87,11 +89,10 @@ class AirGradientButton(AirGradientEntity, ButtonEntity):
|
||||
"""Defines an AirGradient button."""
|
||||
|
||||
entity_description: AirGradientButtonEntityDescription
|
||||
coordinator: AirGradientConfigCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirGradientConfigCoordinator,
|
||||
coordinator: AirGradientCoordinator,
|
||||
description: AirGradientButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize airgradient button."""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -16,7 +17,15 @@ if TYPE_CHECKING:
|
||||
from . import AirGradientConfigEntry
|
||||
|
||||
|
||||
class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
@dataclass
|
||||
class AirGradientData:
|
||||
"""Class for AirGradient data."""
|
||||
|
||||
measures: Measures
|
||||
config: Config
|
||||
|
||||
|
||||
class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
||||
"""Class to manage fetching AirGradient data."""
|
||||
|
||||
config_entry: AirGradientConfigEntry
|
||||
@@ -33,25 +42,11 @@ class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
assert self.config_entry.unique_id
|
||||
self.serial_number = self.config_entry.unique_id
|
||||
|
||||
async def _async_update_data(self) -> _DataT:
|
||||
async def _async_update_data(self) -> AirGradientData:
|
||||
try:
|
||||
return await self._update_data()
|
||||
measures = await self.client.get_current_measures()
|
||||
config = await self.client.get_config()
|
||||
except AirGradientError as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
async def _update_data(self) -> _DataT:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
|
||||
"""Class to manage fetching AirGradient data."""
|
||||
|
||||
async def _update_data(self) -> Measures:
|
||||
return await self.client.get_current_measures()
|
||||
|
||||
|
||||
class AirGradientConfigCoordinator(AirGradientCoordinator[Config]):
|
||||
"""Class to manage fetching AirGradient data."""
|
||||
|
||||
async def _update_data(self) -> Config:
|
||||
return await self.client.get_config()
|
||||
else:
|
||||
return AirGradientData(measures, config)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Base class for AirGradient entities."""
|
||||
|
||||
from airgradient import get_model_name
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -15,6 +17,12 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
|
||||
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
||||
"""Initialize airgradient entity."""
|
||||
super().__init__(coordinator)
|
||||
measures = coordinator.data.measures
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.serial_number)},
|
||||
manufacturer="AirGradient",
|
||||
model=get_model_name(measures.model),
|
||||
model_id=measures.model,
|
||||
serial_number=coordinator.serial_number,
|
||||
sw_version=measures.firmware_version,
|
||||
)
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airgradient==0.9.0"],
|
||||
"requirements": ["airgradient==0.8.0"],
|
||||
"zeroconf": ["_airgradient._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AirGradientConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirGradientConfigCoordinator
|
||||
from .coordinator import AirGradientCoordinator
|
||||
from .entity import AirGradientEntity
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up AirGradient number entities based on a config entry."""
|
||||
|
||||
model = entry.runtime_data.measurement.data.model
|
||||
coordinator = entry.runtime_data.config
|
||||
coordinator = entry.runtime_data
|
||||
model = coordinator.data.measures.model
|
||||
|
||||
added_entities = False
|
||||
|
||||
@@ -72,7 +72,7 @@ async def async_setup_entry(
|
||||
nonlocal added_entities
|
||||
|
||||
if (
|
||||
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||
coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
|
||||
and not added_entities
|
||||
):
|
||||
entities = []
|
||||
@@ -84,7 +84,8 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
added_entities = True
|
||||
elif (
|
||||
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||
coordinator.data.config.configuration_control
|
||||
is not ConfigurationControl.LOCAL
|
||||
and added_entities
|
||||
):
|
||||
entity_registry = er.async_get(hass)
|
||||
@@ -104,11 +105,10 @@ class AirGradientNumber(AirGradientEntity, NumberEntity):
|
||||
"""Defines an AirGradient number entity."""
|
||||
|
||||
entity_description: AirGradientNumberEntityDescription
|
||||
coordinator: AirGradientConfigCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirGradientConfigCoordinator,
|
||||
coordinator: AirGradientCoordinator,
|
||||
description: AirGradientNumberEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize AirGradient number."""
|
||||
@@ -119,7 +119,7 @@ class AirGradientNumber(AirGradientEntity, NumberEntity):
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
"""Return the state of the number."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
return self.entity_description.value_fn(self.coordinator.data.config)
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set the selected value."""
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AirGradientConfigEntry
|
||||
from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE
|
||||
from .coordinator import AirGradientConfigCoordinator
|
||||
from .coordinator import AirGradientCoordinator
|
||||
from .entity import AirGradientEntity
|
||||
|
||||
|
||||
@@ -144,13 +144,11 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up AirGradient select entities based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data.config
|
||||
measurement_coordinator = entry.runtime_data.measurement
|
||||
coordinator = entry.runtime_data
|
||||
model = coordinator.data.measures.model
|
||||
|
||||
async_add_entities([AirGradientSelect(coordinator, CONFIG_CONTROL_ENTITY)])
|
||||
|
||||
model = measurement_coordinator.data.model
|
||||
|
||||
added_entities = False
|
||||
|
||||
@callback
|
||||
@@ -158,7 +156,7 @@ async def async_setup_entry(
|
||||
nonlocal added_entities
|
||||
|
||||
if (
|
||||
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||
coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
|
||||
and not added_entities
|
||||
):
|
||||
entities: list[AirGradientSelect] = [
|
||||
@@ -179,7 +177,8 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
added_entities = True
|
||||
elif (
|
||||
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||
coordinator.data.config.configuration_control
|
||||
is not ConfigurationControl.LOCAL
|
||||
and added_entities
|
||||
):
|
||||
entity_registry = er.async_get(hass)
|
||||
@@ -201,11 +200,10 @@ class AirGradientSelect(AirGradientEntity, SelectEntity):
|
||||
"""Defines an AirGradient select entity."""
|
||||
|
||||
entity_description: AirGradientSelectEntityDescription
|
||||
coordinator: AirGradientConfigCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirGradientConfigCoordinator,
|
||||
coordinator: AirGradientCoordinator,
|
||||
description: AirGradientSelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize AirGradient select."""
|
||||
@@ -216,7 +214,7 @@ class AirGradientSelect(AirGradientEntity, SelectEntity):
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the state of the select."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
return self.entity_description.value_fn(self.coordinator.data.config)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
|
||||
@@ -32,7 +32,7 @@ from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import AirGradientConfigEntry
|
||||
from .const import PM_STANDARD, PM_STANDARD_REVERSE
|
||||
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
|
||||
from .coordinator import AirGradientCoordinator
|
||||
from .entity import AirGradientEntity
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up AirGradient sensor entities based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data.measurement
|
||||
coordinator = entry.runtime_data
|
||||
listener: Callable[[], None] | None = None
|
||||
not_setup: set[AirGradientMeasurementSensorEntityDescription] = set(
|
||||
MEASUREMENT_SENSOR_TYPES
|
||||
@@ -232,7 +232,7 @@ async def async_setup_entry(
|
||||
not_setup = set()
|
||||
sensors = []
|
||||
for description in sensor_descriptions:
|
||||
if description.value_fn(coordinator.data) is None:
|
||||
if description.value_fn(coordinator.data.measures) is None:
|
||||
not_setup.add(description)
|
||||
else:
|
||||
sensors.append(AirGradientMeasurementSensor(coordinator, description))
|
||||
@@ -248,64 +248,65 @@ async def async_setup_entry(
|
||||
add_entities()
|
||||
|
||||
entities = [
|
||||
AirGradientConfigSensor(entry.runtime_data.config, description)
|
||||
AirGradientConfigSensor(coordinator, description)
|
||||
for description in CONFIG_SENSOR_TYPES
|
||||
]
|
||||
if "L" in coordinator.data.model:
|
||||
if "L" in coordinator.data.measures.model:
|
||||
entities.extend(
|
||||
AirGradientConfigSensor(entry.runtime_data.config, description)
|
||||
AirGradientConfigSensor(coordinator, description)
|
||||
for description in CONFIG_LED_BAR_SENSOR_TYPES
|
||||
)
|
||||
if "I" in coordinator.data.model:
|
||||
if "I" in coordinator.data.measures.model:
|
||||
entities.extend(
|
||||
AirGradientConfigSensor(entry.runtime_data.config, description)
|
||||
AirGradientConfigSensor(coordinator, description)
|
||||
for description in CONFIG_DISPLAY_SENSOR_TYPES
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AirGradientMeasurementSensor(AirGradientEntity, SensorEntity):
|
||||
class AirGradientSensor(AirGradientEntity, SensorEntity):
|
||||
"""Defines an AirGradient sensor."""
|
||||
|
||||
entity_description: AirGradientMeasurementSensorEntityDescription
|
||||
coordinator: AirGradientMeasurementCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirGradientMeasurementCoordinator,
|
||||
description: AirGradientMeasurementSensorEntityDescription,
|
||||
coordinator: AirGradientCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize airgradient sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||
|
||||
|
||||
class AirGradientMeasurementSensor(AirGradientSensor):
|
||||
"""Defines an AirGradient sensor."""
|
||||
|
||||
entity_description: AirGradientMeasurementSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
return self.entity_description.value_fn(self.coordinator.data.measures)
|
||||
|
||||
|
||||
class AirGradientConfigSensor(AirGradientEntity, SensorEntity):
|
||||
class AirGradientConfigSensor(AirGradientSensor):
|
||||
"""Defines an AirGradient sensor."""
|
||||
|
||||
entity_description: AirGradientConfigSensorEntityDescription
|
||||
coordinator: AirGradientConfigCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirGradientConfigCoordinator,
|
||||
coordinator: AirGradientCoordinator,
|
||||
description: AirGradientConfigSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize airgradient sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||
super().__init__(coordinator, description)
|
||||
self._attr_entity_registry_enabled_default = (
|
||||
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||
coordinator.data.config.configuration_control
|
||||
is not ConfigurationControl.LOCAL
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
return self.entity_description.value_fn(self.coordinator.data.config)
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AirGradientConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirGradientConfigCoordinator
|
||||
from .coordinator import AirGradientCoordinator
|
||||
from .entity import AirGradientEntity
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AirGradient switch entities based on a config entry."""
|
||||
coordinator = entry.runtime_data.config
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
added_entities = False
|
||||
|
||||
@@ -55,7 +55,7 @@ async def async_setup_entry(
|
||||
nonlocal added_entities
|
||||
|
||||
if (
|
||||
coordinator.data.configuration_control is ConfigurationControl.LOCAL
|
||||
coordinator.data.config.configuration_control is ConfigurationControl.LOCAL
|
||||
and not added_entities
|
||||
):
|
||||
async_add_entities(
|
||||
@@ -63,7 +63,8 @@ async def async_setup_entry(
|
||||
)
|
||||
added_entities = True
|
||||
elif (
|
||||
coordinator.data.configuration_control is not ConfigurationControl.LOCAL
|
||||
coordinator.data.config.configuration_control
|
||||
is not ConfigurationControl.LOCAL
|
||||
and added_entities
|
||||
):
|
||||
entity_registry = er.async_get(hass)
|
||||
@@ -82,11 +83,10 @@ class AirGradientSwitch(AirGradientEntity, SwitchEntity):
|
||||
"""Defines an AirGradient switch entity."""
|
||||
|
||||
entity_description: AirGradientSwitchEntityDescription
|
||||
coordinator: AirGradientConfigCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirGradientConfigCoordinator,
|
||||
coordinator: AirGradientCoordinator,
|
||||
description: AirGradientSwitchEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize AirGradient switch."""
|
||||
@@ -97,7 +97,7 @@ class AirGradientSwitch(AirGradientEntity, SwitchEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the switch."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
return self.entity_description.value_fn(self.coordinator.data.config)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AirGradientConfigEntry, AirGradientMeasurementCoordinator
|
||||
from . import AirGradientConfigEntry, AirGradientCoordinator
|
||||
from .entity import AirGradientEntity
|
||||
|
||||
SCAN_INTERVAL = timedelta(hours=1)
|
||||
@@ -20,18 +20,17 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Airgradient update platform."""
|
||||
|
||||
data = config_entry.runtime_data
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities([AirGradientUpdate(data.measurement)], True)
|
||||
async_add_entities([AirGradientUpdate(coordinator)], True)
|
||||
|
||||
|
||||
class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
||||
"""Representation of Airgradient Update."""
|
||||
|
||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||
coordinator: AirGradientMeasurementCoordinator
|
||||
|
||||
def __init__(self, coordinator: AirGradientMeasurementCoordinator) -> None:
|
||||
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{coordinator.serial_number}-update"
|
||||
@@ -44,7 +43,7 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
||||
@property
|
||||
def installed_version(self) -> str:
|
||||
"""Return the installed version of the entity."""
|
||||
return self.coordinator.data.firmware_version
|
||||
return self.coordinator.data.measures.firmware_version
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity."""
|
||||
|
||||
@@ -53,6 +53,8 @@ from .const import (
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
type AirVisualConfigEntry = ConfigEntry[DataUpdateCoordinator]
|
||||
|
||||
# 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:
|
||||
DOMAIN_AIRVISUAL_PRO = "airvisual_pro"
|
||||
@@ -91,10 +93,9 @@ def async_get_cloud_coordinators_by_api_key(
|
||||
) -> list[DataUpdateCoordinator]:
|
||||
"""Get all DataUpdateCoordinator objects related to a particular API key."""
|
||||
return [
|
||||
coordinator
|
||||
for entry_id, coordinator in hass.data[DOMAIN].items()
|
||||
if (entry := hass.config_entries.async_get_entry(entry_id))
|
||||
and entry.data.get(CONF_API_KEY) == api_key
|
||||
entry.runtime_data
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.data.get(CONF_API_KEY) == api_key and hasattr(entry, "runtime_data")
|
||||
]
|
||||
|
||||
|
||||
@@ -172,7 +173,7 @@ def _standardize_geography_config_entry(
|
||||
hass.config_entries.async_update_entry(entry, **entry_updates)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> bool:
|
||||
"""Set up AirVisual as config entry."""
|
||||
if CONF_API_KEY not in entry.data:
|
||||
# If this is a migrated AirVisual Pro entry, there's no actual setup to do;
|
||||
@@ -220,8 +221,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
# Reassess the interval between 2 server requests
|
||||
async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY])
|
||||
@@ -231,7 +231,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> bool:
|
||||
"""Migrate an old config entry."""
|
||||
version = entry.version
|
||||
|
||||
@@ -388,21 +388,19 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> bool:
|
||||
"""Unload an AirVisual config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if CONF_API_KEY in entry.data:
|
||||
# Re-calculate the update interval period for any remaining consumers of
|
||||
# this API key:
|
||||
async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY])
|
||||
if unload_ok and CONF_API_KEY in entry.data:
|
||||
# Re-calculate the update interval period for any remaining consumers of
|
||||
# this API key:
|
||||
async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY])
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) -> None:
|
||||
"""Handle an options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_COUNTRY,
|
||||
@@ -15,9 +14,9 @@ from homeassistant.const import (
|
||||
CONF_UNIQUE_ID,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import CONF_CITY, DOMAIN
|
||||
from . import AirVisualConfigEntry
|
||||
from .const import CONF_CITY
|
||||
|
||||
CONF_COORDINATES = "coordinates"
|
||||
CONF_TITLE = "title"
|
||||
@@ -37,10 +36,10 @@ TO_REDACT = {
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
hass: HomeAssistant, entry: AirVisualConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
return {
|
||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||
|
||||
@@ -26,8 +26,8 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import AirVisualEntity
|
||||
from .const import CONF_CITY, DOMAIN
|
||||
from . import AirVisualConfigEntry, AirVisualEntity
|
||||
from .const import CONF_CITY
|
||||
|
||||
ATTR_CITY = "city"
|
||||
ATTR_COUNTRY = "country"
|
||||
@@ -105,10 +105,12 @@ POLLUTANT_UNITS = {
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: AirVisualConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AirVisual sensors based on a config entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
AirVisualGeographySensor(coordinator, entry, description, locale)
|
||||
for locale in GEOGRAPHY_SENSOR_LOCALES
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.8.2"]
|
||||
"requirements": ["aioairzone==0.9.1"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.6.2"]
|
||||
"requirements": ["aioairzone-cloud==0.6.5"]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,16 @@ from aioairzone_cloud.const import (
|
||||
AZD_AQ_PM_10,
|
||||
AZD_CPU_USAGE,
|
||||
AZD_HUMIDITY,
|
||||
AZD_INDOOR_EXCHANGER_TEMP,
|
||||
AZD_INDOOR_RETURN_TEMP,
|
||||
AZD_INDOOR_WORK_TEMP,
|
||||
AZD_MEMORY_FREE,
|
||||
AZD_OUTDOOR_CONDENSER_PRESS,
|
||||
AZD_OUTDOOR_DISCHARGE_TEMP,
|
||||
AZD_OUTDOOR_ELECTRIC_CURRENT,
|
||||
AZD_OUTDOOR_EVAPORATOR_PRESS,
|
||||
AZD_OUTDOOR_EXCHANGER_TEMP,
|
||||
AZD_OUTDOOR_TEMP,
|
||||
AZD_TEMP,
|
||||
AZD_THERMOSTAT_BATTERY,
|
||||
AZD_THERMOSTAT_COVERAGE,
|
||||
@@ -32,7 +41,9 @@ from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfInformation,
|
||||
UnitOfPressure,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -48,6 +59,78 @@ from .entity import (
|
||||
)
|
||||
|
||||
AIDOO_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_INDOOR_EXCHANGER_TEMP,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="indoor_exchanger_temp",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_INDOOR_RETURN_TEMP,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="indoor_return_temp",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_INDOOR_WORK_TEMP,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="indoor_work_temp",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_OUTDOOR_CONDENSER_PRESS,
|
||||
native_unit_of_measurement=UnitOfPressure.KPA,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="outdoor_condenser_press",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_OUTDOOR_DISCHARGE_TEMP,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="outdoor_discharge_temp",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_OUTDOOR_ELECTRIC_CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="outdoor_electric_current",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_OUTDOOR_EVAPORATOR_PRESS,
|
||||
native_unit_of_measurement=UnitOfPressure.KPA,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="outdoor_evaporator_press",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_OUTDOOR_EXCHANGER_TEMP,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="outdoor_exchanger_temp",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_OUTDOOR_TEMP,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="outdoor_temp",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key=AZD_TEMP,
|
||||
|
||||
@@ -45,6 +45,33 @@
|
||||
"free_memory": {
|
||||
"name": "Free memory"
|
||||
},
|
||||
"indoor_exchanger_temp": {
|
||||
"name": "Indoor exchanger temperature"
|
||||
},
|
||||
"indoor_return_temp": {
|
||||
"name": "Indoor return temperature"
|
||||
},
|
||||
"indoor_work_temp": {
|
||||
"name": "Indoor working temperature"
|
||||
},
|
||||
"outdoor_condenser_press": {
|
||||
"name": "Outdoor condenser pressure"
|
||||
},
|
||||
"outdoor_discharge_temp": {
|
||||
"name": "Outdoor discharge temperature"
|
||||
},
|
||||
"outdoor_electric_current": {
|
||||
"name": "Outdoor electric current"
|
||||
},
|
||||
"outdoor_evaporator_press": {
|
||||
"name": "Outdoor evaporator pressure"
|
||||
},
|
||||
"outdoor_exchanger_temp": {
|
||||
"name": "Outdoor exchanger temperature"
|
||||
},
|
||||
"outdoor_temp": {
|
||||
"name": "Outdoor temperature"
|
||||
},
|
||||
"thermostat_coverage": {
|
||||
"name": "Signal percentage"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
from typing import Any, Final
|
||||
|
||||
@@ -114,6 +115,8 @@ def get_engine(
|
||||
|
||||
all_voices: dict[str, dict[str, str]] = {}
|
||||
|
||||
all_engines: dict[str, set[str]] = defaultdict(set)
|
||||
|
||||
all_voices_req = polly_client.describe_voices()
|
||||
|
||||
for voice in all_voices_req.get("Voices", []):
|
||||
@@ -124,8 +127,12 @@ def get_engine(
|
||||
language_code: str | None = voice.get("LanguageCode")
|
||||
if language_code is not None and language_code not in supported_languages:
|
||||
supported_languages.append(language_code)
|
||||
for engine in voice.get("SupportedEngines"):
|
||||
all_engines[engine].add(voice_id)
|
||||
|
||||
return AmazonPollyProvider(polly_client, config, supported_languages, all_voices)
|
||||
return AmazonPollyProvider(
|
||||
polly_client, config, supported_languages, all_voices, all_engines
|
||||
)
|
||||
|
||||
|
||||
class AmazonPollyProvider(Provider):
|
||||
@@ -137,13 +144,16 @@ class AmazonPollyProvider(Provider):
|
||||
config: ConfigType,
|
||||
supported_languages: list[str],
|
||||
all_voices: dict[str, dict[str, str]],
|
||||
all_engines: dict[str, set[str]],
|
||||
) -> None:
|
||||
"""Initialize Amazon Polly provider for TTS."""
|
||||
self.client = polly_client
|
||||
self.config = config
|
||||
self.supported_langs = supported_languages
|
||||
self.all_voices = all_voices
|
||||
self.all_engines = all_engines
|
||||
self.default_voice: str = self.config[CONF_VOICE]
|
||||
self.default_engine: str = self.config[CONF_ENGINE]
|
||||
self.name = "Amazon Polly"
|
||||
|
||||
@property
|
||||
@@ -159,12 +169,12 @@ class AmazonPollyProvider(Provider):
|
||||
@property
|
||||
def default_options(self) -> dict[str, str]:
|
||||
"""Return dict include default options."""
|
||||
return {CONF_VOICE: self.default_voice}
|
||||
return {CONF_VOICE: self.default_voice, CONF_ENGINE: self.default_engine}
|
||||
|
||||
@property
|
||||
def supported_options(self) -> list[str]:
|
||||
"""Return a list of supported options."""
|
||||
return [CONF_VOICE]
|
||||
return [CONF_VOICE, CONF_ENGINE]
|
||||
|
||||
def get_tts_audio(
|
||||
self,
|
||||
@@ -179,9 +189,14 @@ class AmazonPollyProvider(Provider):
|
||||
_LOGGER.error("%s does not support the %s language", voice_id, language)
|
||||
return None, None
|
||||
|
||||
engine = options.get(CONF_ENGINE, self.default_engine)
|
||||
if voice_id not in self.all_engines[engine]:
|
||||
_LOGGER.error("%s does not support the %s engine", voice_id, engine)
|
||||
return None, None
|
||||
|
||||
_LOGGER.debug("Requesting TTS file for text: %s", message)
|
||||
resp = self.client.synthesize_speech(
|
||||
Engine=self.config[CONF_ENGINE],
|
||||
Engine=engine,
|
||||
OutputFormat=self.config[CONF_OUTPUT_FORMAT],
|
||||
SampleRate=self.config[CONF_SAMPLE_RATE],
|
||||
Text=message,
|
||||
|
||||
@@ -7,11 +7,13 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_SITE_ID, DOMAIN, PLATFORMS
|
||||
from .const import CONF_SITE_ID, PLATFORMS
|
||||
from .coordinator import AmberUpdateCoordinator
|
||||
|
||||
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool:
|
||||
"""Set up Amber Electric from a config entry."""
|
||||
configuration = Configuration(access_token=entry.data[CONF_API_TOKEN])
|
||||
api_instance = amber_api.AmberApi.create(configuration)
|
||||
@@ -19,15 +21,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
coordinator = AmberUpdateCoordinator(hass, api_instance, site_id)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
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: AmberConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -8,12 +8,12 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, DOMAIN
|
||||
from . import AmberConfigEntry
|
||||
from .const import ATTRIBUTION
|
||||
from .coordinator import AmberUpdateCoordinator
|
||||
|
||||
PRICE_SPIKE_ICONS = {
|
||||
@@ -85,11 +85,11 @@ class AmberDemandWindowBinarySensor(AmberPriceGridSensor):
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AmberConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a config entry."""
|
||||
coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
price_spike_description = BinarySensorEntityDescription(
|
||||
key="price_spike",
|
||||
|
||||
@@ -17,13 +17,13 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CURRENCY_DOLLAR, PERCENTAGE, UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, DOMAIN
|
||||
from . import AmberConfigEntry
|
||||
from .const import ATTRIBUTION
|
||||
from .coordinator import AmberUpdateCoordinator, normalize_descriptor
|
||||
|
||||
UNIT = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}"
|
||||
@@ -196,11 +196,11 @@ class AmberGridSensor(CoordinatorEntity[AmberUpdateCoordinator], SensorEntity):
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AmberConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a config entry."""
|
||||
coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
current: dict[str, CurrentInterval] = coordinator.data["current"]
|
||||
forecasts: dict[str, list[ForecastInterval]] = coordinator.data["forecasts"]
|
||||
|
||||
@@ -8,28 +8,30 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AmbientNetworkDataUpdateCoordinator
|
||||
|
||||
type AmbientNetworkConfigEntry = ConfigEntry[AmbientNetworkDataUpdateCoordinator]
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AmbientNetworkConfigEntry
|
||||
) -> bool:
|
||||
"""Set up the Ambient Weather Network from a config entry."""
|
||||
|
||||
api = OpenAPI()
|
||||
coordinator = AmbientNetworkDataUpdateCoordinator(hass, api)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
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: AmbientNetworkConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -10,7 +10,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
@@ -29,7 +28,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import AmbientNetworkConfigEntry
|
||||
from .coordinator import AmbientNetworkDataUpdateCoordinator
|
||||
from .entity import AmbientNetworkEntity
|
||||
|
||||
@@ -271,12 +270,12 @@ SENSOR_DESCRIPTIONS = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AmbientNetworkConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Ambient Network sensor entities."""
|
||||
|
||||
coordinator: AmbientNetworkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
if coordinator.config_entry is not None:
|
||||
async_add_entities(
|
||||
AmbientNetworkSensor(
|
||||
|
||||
@@ -13,22 +13,20 @@ from anova_wifi import (
|
||||
WebsocketFailure,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AnovaCoordinator
|
||||
from .models import AnovaData
|
||||
from .models import AnovaConfigEntry, AnovaData
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AnovaConfigEntry) -> bool:
|
||||
"""Set up Anova from a config entry."""
|
||||
api = AnovaApi(
|
||||
aiohttp_client.async_get_clientsession(hass),
|
||||
@@ -62,17 +60,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
assert api.websocket_handler is not None
|
||||
devices = list(api.websocket_handler.devices.values())
|
||||
coordinators = [AnovaCoordinator(hass, device) for device in devices]
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AnovaData(
|
||||
api_jwt=api.jwt, coordinators=coordinators, api=api
|
||||
)
|
||||
entry.runtime_data = AnovaData(api_jwt=api.jwt, coordinators=coordinators, api=api)
|
||||
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: AnovaConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
anova_data: AnovaData = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
# Disconnect from WS
|
||||
await anova_data.api.disconnect_websocket()
|
||||
await entry.runtime_data.api.disconnect_websocket()
|
||||
return unload_ok
|
||||
|
||||
@@ -4,8 +4,12 @@ from dataclasses import dataclass
|
||||
|
||||
from anova_wifi import AnovaApi
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from .coordinator import AnovaCoordinator
|
||||
|
||||
type AnovaConfigEntry = ConfigEntry[AnovaData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnovaData:
|
||||
|
||||
@@ -7,7 +7,6 @@ from dataclasses import dataclass
|
||||
|
||||
from anova_wifi import AnovaMode, AnovaState, APCUpdateSensor
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
@@ -19,10 +18,9 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AnovaCoordinator
|
||||
from .entity import AnovaDescriptionEntity
|
||||
from .models import AnovaData
|
||||
from .models import AnovaConfigEntry
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -99,11 +97,11 @@ SENSOR_DESCRIPTIONS: list[AnovaSensorEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
entry: AnovaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Anova device."""
|
||||
anova_data: AnovaData = hass.data[DOMAIN][entry.entry_id]
|
||||
anova_data = entry.runtime_data
|
||||
|
||||
for coordinator in anova_data.coordinators:
|
||||
setup_coordinator(coordinator, async_add_entities)
|
||||
|
||||
@@ -13,14 +13,16 @@ from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import ANTHEMAV_UPDATE_SIGNAL, DEVICE_TIMEOUT_SECONDS, DOMAIN
|
||||
from .const import ANTHEMAV_UPDATE_SIGNAL, DEVICE_TIMEOUT_SECONDS
|
||||
|
||||
type AnthemavConfigEntry = ConfigEntry[anthemav.Connection]
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AnthemavConfigEntry) -> bool:
|
||||
"""Set up Anthem A/V Receivers from a config entry."""
|
||||
|
||||
@callback
|
||||
@@ -41,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except (OSError, DeviceError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = avr
|
||||
entry.runtime_data = avr
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -56,16 +58,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AnthemavConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
avr = hass.data[DOMAIN][entry.entry_id]
|
||||
avr = entry.runtime_data
|
||||
_LOGGER.debug("Close avr connection")
|
||||
avr.close()
|
||||
|
||||
if avr is not None:
|
||||
_LOGGER.debug("Close avr connection")
|
||||
avr.close()
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from anthemav.connection import Connection
|
||||
from anthemav.protocol import AVR
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -13,13 +12,13 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MAC, CONF_MODEL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AnthemavConfigEntry
|
||||
from .const import ANTHEMAV_UPDATE_SIGNAL, DOMAIN, MANUFACTURER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -27,7 +26,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AnthemavConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up entry."""
|
||||
@@ -35,7 +34,7 @@ async def async_setup_entry(
|
||||
mac_address = config_entry.data[CONF_MAC]
|
||||
model = config_entry.data[CONF_MODEL]
|
||||
|
||||
avr: Connection = hass.data[DOMAIN][config_entry.entry_id]
|
||||
avr = config_entry.runtime_data
|
||||
|
||||
_LOGGER.debug("Connection data dump: %s", avr.dump_conndata)
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ from .coordinator import AOSmithEnergyCoordinator, AOSmithStatusCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.WATER_HEATER]
|
||||
|
||||
type AOSmithConfigEntry = ConfigEntry[AOSmithData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AOSmithData:
|
||||
@@ -26,7 +28,7 @@ class AOSmithData:
|
||||
energy_coordinator: AOSmithEnergyCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AOSmithConfigEntry) -> bool:
|
||||
"""Set up A. O. Smith from a config entry."""
|
||||
email = entry.data[CONF_EMAIL]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
@@ -55,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
await energy_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AOSmithData(
|
||||
entry.runtime_data = AOSmithData(
|
||||
client,
|
||||
status_coordinator,
|
||||
energy_coordinator,
|
||||
@@ -66,9 +68,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AOSmithConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -5,11 +5,9 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AOSmithData
|
||||
from .const import DOMAIN
|
||||
from . import AOSmithConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
"address",
|
||||
@@ -31,10 +29,10 @@ TO_REDACT = {
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: AOSmithConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
data: AOSmithData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
data = config_entry.runtime_data
|
||||
|
||||
all_device_info = await data.client.get_all_device_info()
|
||||
return async_redact_data(all_device_info, TO_REDACT)
|
||||
|
||||
@@ -11,13 +11,11 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AOSmithData
|
||||
from .const import DOMAIN
|
||||
from . import AOSmithConfigEntry
|
||||
from .coordinator import AOSmithEnergyCoordinator, AOSmithStatusCoordinator
|
||||
from .entity import AOSmithEnergyEntity, AOSmithStatusEntity
|
||||
|
||||
@@ -49,10 +47,12 @@ HOT_WATER_STATUS_MAP: dict[HotWaterStatus, str] = {
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: AOSmithConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up A. O. Smith sensor platform."""
|
||||
data: AOSmithData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AOSmithStatusSensorEntity(data.status_coordinator, description, junction_id)
|
||||
|
||||
@@ -12,14 +12,12 @@ from homeassistant.components.water_heater import (
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AOSmithData
|
||||
from .const import DOMAIN
|
||||
from . import AOSmithConfigEntry
|
||||
from .coordinator import AOSmithStatusCoordinator
|
||||
from .entity import AOSmithStatusEntity
|
||||
|
||||
@@ -46,10 +44,12 @@ DEFAULT_OPERATION_MODE_PRIORITY = [
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: AOSmithConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up A. O. Smith water heater platform."""
|
||||
data: AOSmithData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AOSmithWaterHeaterEntity(data.status_coordinator, junction_id)
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import APCUPSdCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
type APCUPSdConfigEntry = ConfigEntry[APCUPSdCoordinator]
|
||||
|
||||
PLATFORMS: Final = (Platform.BINARY_SENSOR, Platform.SENSOR)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: APCUPSdConfigEntry
|
||||
) -> bool:
|
||||
"""Use config values to set up a function enabling status retrieval."""
|
||||
host, port = config_entry.data[CONF_HOST], config_entry.data[CONF_PORT]
|
||||
coordinator = APCUPSdCoordinator(hass, host, port)
|
||||
@@ -25,17 +25,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Store the coordinator for later uses.
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][config_entry.entry_id] = coordinator
|
||||
config_entry.runtime_data = coordinator
|
||||
|
||||
# Forward the config entries to the supported platforms.
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: APCUPSdConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok and DOMAIN in hass.data:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -2,24 +2,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import APCUPSdConfigEntry
|
||||
from .coordinator import APCUPSdCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_DESCRIPTION = BinarySensorEntityDescription(
|
||||
key="statflag",
|
||||
translation_key="online_status",
|
||||
@@ -30,11 +27,11 @@ _VALUE_ONLINE_MASK: Final = 0b1000
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: APCUPSdConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up an APCUPSd Online Status binary sensor."""
|
||||
coordinator: APCUPSdCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
# Do not create the binary sensor if APCUPSd does not provide STATFLAG field for us
|
||||
# to determine the online status.
|
||||
|
||||
@@ -5,19 +5,17 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import APCUPSdCoordinator, APCUPSdData
|
||||
from . import APCUPSdConfigEntry
|
||||
|
||||
TO_REDACT = {"SERIALNO", "HOSTNAME"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
hass: HomeAssistant, entry: APCUPSdConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: APCUPSdCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
data: APCUPSdData = coordinator.data
|
||||
coordinator = entry.runtime_data
|
||||
data = coordinator.data
|
||||
return async_redact_data(data, TO_REDACT)
|
||||
|
||||
@@ -10,7 +10,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
UnitOfApparentPower,
|
||||
@@ -25,7 +24,8 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, LAST_S_TEST
|
||||
from . import APCUPSdConfigEntry
|
||||
from .const import LAST_S_TEST
|
||||
from .coordinator import APCUPSdCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -406,11 +406,11 @@ INFERRED_UNITS = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: APCUPSdConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the APCUPSd sensors from config entries."""
|
||||
coordinator: APCUPSdCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
# The resource keys in the data dict collected in the coordinator is in upper-case
|
||||
# by default, but we use lower cases throughout this integration.
|
||||
|
||||
@@ -4,12 +4,13 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from aioaseko import Aseko, AsekoNotLoggedIn
|
||||
from aioaseko import APIUnavailable, InvalidAuthCredentials, MobileAccount
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AsekoDataUpdateCoordinator
|
||||
@@ -21,17 +22,28 @@ PLATFORMS: list[str] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Aseko Pool Live from a config entry."""
|
||||
aseko = Aseko(entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD])
|
||||
account = MobileAccount(
|
||||
async_get_clientsession(hass),
|
||||
username=entry.data[CONF_EMAIL],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
await aseko.login()
|
||||
except AsekoNotLoggedIn as err:
|
||||
units = await account.get_units()
|
||||
except InvalidAuthCredentials as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except APIUnavailable as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = []
|
||||
|
||||
for unit in units:
|
||||
coordinator = AsekoDataUpdateCoordinator(hass, unit)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN][entry.entry_id].append((unit, coordinator))
|
||||
|
||||
coordinator = AsekoDataUpdateCoordinator(hass, aseko)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -39,6 +51,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from dataclasses import dataclass
|
||||
from aioaseko import Unit
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
@@ -24,14 +25,26 @@ from .entity import AsekoEntity
|
||||
class AsekoBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes an Aseko binary sensor entity."""
|
||||
|
||||
value_fn: Callable[[Unit], bool | None]
|
||||
value_fn: Callable[[Unit], bool]
|
||||
|
||||
|
||||
BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
|
||||
UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
|
||||
AsekoBinarySensorEntityDescription(
|
||||
key="water_flow",
|
||||
translation_key="water_flow_to_probes",
|
||||
value_fn=lambda unit: unit.water_flow_to_probes,
|
||||
translation_key="water_flow",
|
||||
value_fn=lambda unit: unit.water_flow,
|
||||
),
|
||||
AsekoBinarySensorEntityDescription(
|
||||
key="has_alarm",
|
||||
translation_key="alarm",
|
||||
value_fn=lambda unit: unit.has_alarm,
|
||||
device_class=BinarySensorDeviceClass.SAFETY,
|
||||
),
|
||||
AsekoBinarySensorEntityDescription(
|
||||
key="has_error",
|
||||
translation_key="error",
|
||||
value_fn=lambda unit: unit.has_error,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -42,22 +55,33 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Aseko Pool Live binary sensors."""
|
||||
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
units = coordinator.data.values()
|
||||
data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
async_add_entities(
|
||||
AsekoBinarySensorEntity(unit, coordinator, description)
|
||||
for description in BINARY_SENSORS
|
||||
for unit in units
|
||||
if description.value_fn(unit) is not None
|
||||
AsekoUnitBinarySensorEntity(unit, coordinator, description)
|
||||
for unit, coordinator in data
|
||||
for description in UNIT_BINARY_SENSORS
|
||||
)
|
||||
|
||||
|
||||
class AsekoBinarySensorEntity(AsekoEntity, BinarySensorEntity):
|
||||
"""Representation of an Aseko binary sensor entity."""
|
||||
class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity):
|
||||
"""Representation of a unit water flow binary sensor entity."""
|
||||
|
||||
entity_description: AsekoBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unit: Unit,
|
||||
coordinator: AsekoDataUpdateCoordinator,
|
||||
entity_description: AsekoBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the unit binary sensor."""
|
||||
super().__init__(unit, coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.unit)
|
||||
return self.entity_description.value_fn(self._unit)
|
||||
|
||||
@@ -6,11 +6,12 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioaseko import Aseko, AsekoAPIError, AsekoInvalidCredentials
|
||||
from aioaseko import APIUnavailable, InvalidAuthCredentials, WebAccount
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -33,12 +34,15 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def get_account_info(self, email: str, password: str) -> dict:
|
||||
"""Get account info from the mobile API and the web API."""
|
||||
aseko = Aseko(email, password)
|
||||
user = await aseko.login()
|
||||
session = async_get_clientsession(self.hass)
|
||||
|
||||
web_account = WebAccount(session, email, password)
|
||||
web_account_info = await web_account.login()
|
||||
|
||||
return {
|
||||
CONF_EMAIL: email,
|
||||
CONF_PASSWORD: password,
|
||||
CONF_UNIQUE_ID: user.user_id,
|
||||
CONF_UNIQUE_ID: web_account_info.user_id,
|
||||
}
|
||||
|
||||
async def async_step_user(
|
||||
@@ -54,9 +58,9 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
info = await self.get_account_info(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||
)
|
||||
except AsekoAPIError:
|
||||
except APIUnavailable:
|
||||
errors["base"] = "cannot_connect"
|
||||
except AsekoInvalidCredentials:
|
||||
except InvalidAuthCredentials:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
@@ -118,9 +122,9 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
info = await self.get_account_info(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||
)
|
||||
except AsekoAPIError:
|
||||
except APIUnavailable:
|
||||
errors["base"] = "cannot_connect"
|
||||
except AsekoInvalidCredentials:
|
||||
except InvalidAuthCredentials:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
|
||||
@@ -5,31 +5,34 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aioaseko import Aseko, Unit
|
||||
from aioaseko import Unit, Variable
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Unit]]):
|
||||
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Variable]]):
|
||||
"""Class to manage fetching Aseko unit data from single endpoint."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, aseko: Aseko) -> None:
|
||||
def __init__(self, hass: HomeAssistant, unit: Unit) -> None:
|
||||
"""Initialize global Aseko unit data updater."""
|
||||
self._aseko = aseko
|
||||
self._unit = unit
|
||||
|
||||
if self._unit.name:
|
||||
name = self._unit.name
|
||||
else:
|
||||
name = f"{self._unit.type}-{self._unit.serial_number}"
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
name=name,
|
||||
update_interval=timedelta(minutes=2),
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Unit]:
|
||||
async def _async_update_data(self) -> dict[str, Variable]:
|
||||
"""Fetch unit data."""
|
||||
units = await self._aseko.get_units()
|
||||
return {unit.serial_number: unit for unit in units}
|
||||
await self._unit.get_state()
|
||||
return {variable.type: variable for variable in self._unit.variables}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from aioaseko import Unit
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -15,44 +14,20 @@ class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unit: Unit,
|
||||
coordinator: AsekoDataUpdateCoordinator,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None:
|
||||
"""Initialize the aseko entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._unit = unit
|
||||
self._attr_unique_id = f"{self.unit.serial_number}{self.entity_description.key}"
|
||||
|
||||
if self._unit.type == "Remote":
|
||||
self._device_model = "ASIN Pool"
|
||||
else:
|
||||
self._device_model = f"ASIN AQUA {self._unit.type}"
|
||||
self._device_name = self._unit.name if self._unit.name else self._device_model
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.unit.serial_number)},
|
||||
serial_number=self.unit.serial_number,
|
||||
name=unit.name or unit.serial_number,
|
||||
manufacturer=(
|
||||
self.unit.brand_name.primary
|
||||
if self.unit.brand_name is not None
|
||||
else None
|
||||
),
|
||||
model=(
|
||||
self.unit.brand_name.secondary
|
||||
if self.unit.brand_name is not None
|
||||
else None
|
||||
),
|
||||
configuration_url=f"https://aseko.cloud/unit/{self.unit.serial_number}",
|
||||
)
|
||||
|
||||
@property
|
||||
def unit(self) -> Unit:
|
||||
"""Return the aseko unit."""
|
||||
return self.coordinator.data[self._unit.serial_number]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
super().available
|
||||
and self.unit.serial_number in self.coordinator.data
|
||||
and self.unit.online
|
||||
name=self._device_name,
|
||||
identifiers={(DOMAIN, str(self._unit.serial_number))},
|
||||
manufacturer="Aseko",
|
||||
model=self._device_model,
|
||||
)
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"water_flow_to_probes": {
|
||||
"water_flow": {
|
||||
"default": "mdi:waves-arrow-right"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"air_temperature": {
|
||||
"default": "mdi:thermometer-lines"
|
||||
},
|
||||
"free_chlorine": {
|
||||
"default": "mdi:pool"
|
||||
},
|
||||
"redox": {
|
||||
"default": "mdi:pool"
|
||||
},
|
||||
"salinity": {
|
||||
"default": "mdi:pool"
|
||||
"default": "mdi:flask"
|
||||
},
|
||||
"water_temperature": {
|
||||
"default": "mdi:pool-thermometer"
|
||||
"default": "mdi:coolant-temperature"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioaseko"],
|
||||
"requirements": ["aioaseko==1.0.0"]
|
||||
"requirements": ["aioaseko==0.2.0"]
|
||||
}
|
||||
|
||||
@@ -2,104 +2,77 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aioaseko import Unit
|
||||
from aioaseko import Unit, Variable
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfElectricPotential, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AsekoDataUpdateCoordinator
|
||||
from .entity import AsekoEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AsekoSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Aseko sensor entity."""
|
||||
|
||||
value_fn: Callable[[Unit], StateType]
|
||||
|
||||
|
||||
SENSORS: list[AsekoSensorEntityDescription] = [
|
||||
AsekoSensorEntityDescription(
|
||||
key="airTemp",
|
||||
translation_key="air_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.air_temperature,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="free_chlorine",
|
||||
translation_key="free_chlorine",
|
||||
native_unit_of_measurement="mg/l",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.cl_free,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="ph",
|
||||
device_class=SensorDeviceClass.PH,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.ph,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="rx",
|
||||
translation_key="redox",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.redox,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="salinity",
|
||||
translation_key="salinity",
|
||||
native_unit_of_measurement="kg/m³",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.salinity,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="waterTemp",
|
||||
translation_key="water_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.water_temperature,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Aseko Pool Live sensors."""
|
||||
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
units = coordinator.data.values()
|
||||
data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
|
||||
async_add_entities(
|
||||
AsekoSensorEntity(unit, coordinator, description)
|
||||
for description in SENSORS
|
||||
for unit in units
|
||||
if description.value_fn(unit) is not None
|
||||
VariableSensorEntity(unit, variable, coordinator)
|
||||
for unit, coordinator in data
|
||||
for variable in unit.variables
|
||||
)
|
||||
|
||||
|
||||
class AsekoSensorEntity(AsekoEntity, SensorEntity):
|
||||
"""Representation of an Aseko unit sensor entity."""
|
||||
class VariableSensorEntity(AsekoEntity, SensorEntity):
|
||||
"""Representation of a unit variable sensor entity."""
|
||||
|
||||
entity_description: AsekoSensorEntityDescription
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(
|
||||
self, unit: Unit, variable: Variable, coordinator: AsekoDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Initialize the variable sensor."""
|
||||
super().__init__(unit, coordinator)
|
||||
self._variable = variable
|
||||
|
||||
translation_key = {
|
||||
"Air temp.": "air_temperature",
|
||||
"Cl free": "free_chlorine",
|
||||
"Water temp.": "water_temperature",
|
||||
}.get(self._variable.name)
|
||||
if translation_key is not None:
|
||||
self._attr_translation_key = translation_key
|
||||
else:
|
||||
self._attr_name = self._variable.name
|
||||
|
||||
self._attr_unique_id = f"{self._unit.serial_number}{self._variable.type}"
|
||||
self._attr_native_unit_of_measurement = self._variable.unit
|
||||
|
||||
self._attr_icon = {
|
||||
"rx": "mdi:test-tube",
|
||||
"waterLevel": "mdi:waves",
|
||||
}.get(self._variable.type)
|
||||
|
||||
self._attr_device_class = {
|
||||
"airTemp": SensorDeviceClass.TEMPERATURE,
|
||||
"waterTemp": SensorDeviceClass.TEMPERATURE,
|
||||
"ph": SensorDeviceClass.PH,
|
||||
}.get(self._variable.type)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
def native_value(self) -> int | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.unit)
|
||||
variable = self.coordinator.data[self._variable.type]
|
||||
return variable.current_value
|
||||
|
||||
@@ -26,8 +26,11 @@
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"water_flow_to_probes": {
|
||||
"name": "Water flow to probes"
|
||||
"water_flow": {
|
||||
"name": "Water flow"
|
||||
},
|
||||
"alarm": {
|
||||
"name": "Alarm"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -37,12 +40,6 @@
|
||||
"free_chlorine": {
|
||||
"name": "Free chlorine"
|
||||
},
|
||||
"redox": {
|
||||
"name": "Redox potential"
|
||||
},
|
||||
"salinity": {
|
||||
"name": "Salinity"
|
||||
},
|
||||
"water_temperature": {
|
||||
"name": "Water temperature"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -16,6 +17,7 @@ from .const import (
|
||||
DATA_LAST_WAKE_UP,
|
||||
DOMAIN,
|
||||
EVENT_RECORDING,
|
||||
OPTION_PREFERRED,
|
||||
SAMPLE_CHANNELS,
|
||||
SAMPLE_RATE,
|
||||
SAMPLE_WIDTH,
|
||||
@@ -57,6 +59,7 @@ __all__ = (
|
||||
"PipelineNotFound",
|
||||
"WakeWordSettings",
|
||||
"EVENT_RECORDING",
|
||||
"OPTION_PREFERRED",
|
||||
"SAMPLES_PER_CHUNK",
|
||||
"SAMPLE_RATE",
|
||||
"SAMPLE_WIDTH",
|
||||
@@ -99,7 +102,7 @@ async def async_pipeline_from_audio_stream(
|
||||
wake_word_phrase: str | None = None,
|
||||
pipeline_id: str | None = None,
|
||||
conversation_id: str | None = None,
|
||||
tts_audio_output: str | None = None,
|
||||
tts_audio_output: str | dict[str, Any] | None = None,
|
||||
wake_word_settings: WakeWordSettings | None = None,
|
||||
audio_settings: AudioSettings | None = None,
|
||||
device_id: str | None = None,
|
||||
|
||||
@@ -22,3 +22,5 @@ SAMPLE_CHANNELS = 1 # mono
|
||||
MS_PER_CHUNK = 10
|
||||
SAMPLES_PER_CHUNK = SAMPLE_RATE // (1000 // MS_PER_CHUNK) # 10 ms @ 16Khz
|
||||
BYTES_PER_CHUNK = SAMPLES_PER_CHUNK * SAMPLE_WIDTH * SAMPLE_CHANNELS # 16-bit
|
||||
|
||||
OPTION_PREFERRED = "preferred"
|
||||
|
||||
@@ -504,7 +504,7 @@ class AudioSettings:
|
||||
is_vad_enabled: bool = True
|
||||
"""True if VAD is used to determine the end of the voice command."""
|
||||
|
||||
silence_seconds: float = 0.5
|
||||
silence_seconds: float = 0.7
|
||||
"""Seconds of silence after voice command has ended."""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -538,7 +538,7 @@ class PipelineRun:
|
||||
language: str = None # type: ignore[assignment]
|
||||
runner_data: Any | None = None
|
||||
intent_agent: str | None = None
|
||||
tts_audio_output: str | None = None
|
||||
tts_audio_output: str | dict[str, Any] | None = None
|
||||
wake_word_settings: WakeWordSettings | None = None
|
||||
audio_settings: AudioSettings = field(default_factory=AudioSettings)
|
||||
|
||||
@@ -906,6 +906,8 @@ class PipelineRun:
|
||||
metadata,
|
||||
self._speech_to_text_stream(audio_stream=stream, stt_vad=stt_vad),
|
||||
)
|
||||
except (asyncio.CancelledError, TimeoutError):
|
||||
raise # expected
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during speech-to-text")
|
||||
raise SpeechToTextError(
|
||||
@@ -1052,12 +1054,15 @@ class PipelineRun:
|
||||
if self.pipeline.tts_voice is not None:
|
||||
tts_options[tts.ATTR_VOICE] = self.pipeline.tts_voice
|
||||
|
||||
if self.tts_audio_output is not None:
|
||||
if isinstance(self.tts_audio_output, dict):
|
||||
tts_options.update(self.tts_audio_output)
|
||||
elif isinstance(self.tts_audio_output, str):
|
||||
tts_options[tts.ATTR_PREFERRED_FORMAT] = self.tts_audio_output
|
||||
if self.tts_audio_output == "wav":
|
||||
# 16 Khz, 16-bit mono
|
||||
tts_options[tts.ATTR_PREFERRED_SAMPLE_RATE] = SAMPLE_RATE
|
||||
tts_options[tts.ATTR_PREFERRED_SAMPLE_CHANNELS] = SAMPLE_CHANNELS
|
||||
tts_options[tts.ATTR_PREFERRED_SAMPLE_BYTES] = SAMPLE_WIDTH
|
||||
|
||||
try:
|
||||
options_supported = await tts.async_support_options(
|
||||
|
||||
@@ -9,12 +9,10 @@ from homeassistant.const import EntityCategory, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import collection, entity_registry as er, restore_state
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, OPTION_PREFERRED
|
||||
from .pipeline import AssistDevice, PipelineData, PipelineStorageCollection
|
||||
from .vad import VadSensitivity
|
||||
|
||||
OPTION_PREFERRED = "preferred"
|
||||
|
||||
|
||||
@callback
|
||||
def get_chosen_pipeline(
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Base class for assist satellite entities."""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, AssistSatelliteEntityFeature
|
||||
from .entity import AssistSatelliteEntity, AssistSatelliteEntityDescription
|
||||
from .errors import SatelliteBusyError
|
||||
from .websocket_api import async_register_websocket_api
|
||||
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
"AssistSatelliteEntity",
|
||||
"AssistSatelliteEntityDescription",
|
||||
"AssistSatelliteEntityFeature",
|
||||
"SatelliteBusyError",
|
||||
]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
component = hass.data[DOMAIN] = EntityComponent[AssistSatelliteEntity](
|
||||
_LOGGER, DOMAIN, hass
|
||||
)
|
||||
await component.async_setup(config)
|
||||
|
||||
component.async_register_entity_service(
|
||||
"announce",
|
||||
vol.All(
|
||||
cv.make_entity_service_schema(
|
||||
{
|
||||
vol.Optional("message"): str,
|
||||
vol.Optional("media_id"): str,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key("message", "media_id"),
|
||||
),
|
||||
"async_internal_announce",
|
||||
[AssistSatelliteEntityFeature.ANNOUNCE],
|
||||
)
|
||||
async_register_websocket_api(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
component: EntityComponent[AssistSatelliteEntity] = hass.data[DOMAIN]
|
||||
return await component.async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
component: EntityComponent[AssistSatelliteEntity] = hass.data[DOMAIN]
|
||||
return await component.async_unload_entry(entry)
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Constants for assist satellite."""
|
||||
|
||||
from enum import IntFlag
|
||||
|
||||
DOMAIN = "assist_satellite"
|
||||
|
||||
|
||||
class AssistSatelliteEntityFeature(IntFlag):
|
||||
"""Supported features of Assist satellite entity."""
|
||||
|
||||
ANNOUNCE = 1
|
||||
"""Device supports remotely triggered announcements."""
|
||||
@@ -0,0 +1,341 @@
|
||||
"""Assist satellite entity."""
|
||||
|
||||
from abc import abstractmethod
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterable
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Final, final
|
||||
|
||||
from homeassistant.components import media_source, stt, tts
|
||||
from homeassistant.components.assist_pipeline import (
|
||||
OPTION_PREFERRED,
|
||||
AudioSettings,
|
||||
PipelineEvent,
|
||||
PipelineEventType,
|
||||
PipelineStage,
|
||||
async_get_pipeline,
|
||||
async_get_pipelines,
|
||||
async_pipeline_from_audio_stream,
|
||||
vad,
|
||||
)
|
||||
from homeassistant.components.media_player import async_process_play_media_url
|
||||
from homeassistant.components.tts.media_source import (
|
||||
generate_media_source_id as tts_generate_media_source_id,
|
||||
)
|
||||
from homeassistant.core import Context, callback
|
||||
from homeassistant.helpers import entity
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.util import ulid
|
||||
|
||||
from .const import AssistSatelliteEntityFeature
|
||||
from .errors import AssistSatelliteError, SatelliteBusyError
|
||||
|
||||
_CONVERSATION_TIMEOUT_SEC: Final = 5 * 60 # 5 minutes
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AssistSatelliteState(StrEnum):
|
||||
"""Valid states of an Assist satellite entity."""
|
||||
|
||||
LISTENING_WAKE_WORD = "listening_wake_word"
|
||||
"""Device is streaming audio for wake word detection to Home Assistant."""
|
||||
|
||||
LISTENING_COMMAND = "listening_command"
|
||||
"""Device is streaming audio with the voice command to Home Assistant."""
|
||||
|
||||
PROCESSING = "processing"
|
||||
"""Home Assistant is processing the voice command."""
|
||||
|
||||
RESPONDING = "responding"
|
||||
"""Device is speaking the response."""
|
||||
|
||||
|
||||
class AssistSatelliteEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||
"""A class that describes Assist satellite entities."""
|
||||
|
||||
|
||||
class AssistSatelliteEntity(entity.Entity):
|
||||
"""Entity encapsulating the state and functionality of an Assist satellite."""
|
||||
|
||||
entity_description: AssistSatelliteEntityDescription
|
||||
_attr_should_poll = False
|
||||
_attr_supported_features = AssistSatelliteEntityFeature(0)
|
||||
_attr_pipeline_entity_id: str | None = None
|
||||
_attr_vad_sensitivity_entity_id: str | None = None
|
||||
|
||||
_conversation_id: str | None = None
|
||||
_conversation_id_time: float | None = None
|
||||
|
||||
_run_has_tts: bool = False
|
||||
_is_announcing = False
|
||||
_wake_word_intercept_future: asyncio.Future[str | None] | None = None
|
||||
_attr_tts_options: dict[str, Any] | None = None
|
||||
|
||||
__assist_satellite_state = AssistSatelliteState.LISTENING_WAKE_WORD
|
||||
|
||||
@final
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return state of the entity."""
|
||||
return self.__assist_satellite_state
|
||||
|
||||
@property
|
||||
def pipeline_entity_id(self) -> str | None:
|
||||
"""Entity ID of the pipeline to use for the next conversation."""
|
||||
return self._attr_pipeline_entity_id
|
||||
|
||||
@property
|
||||
def vad_sensitivity_entity_id(self) -> str | None:
|
||||
"""Entity ID of the VAD sensitivity to use for the next conversation."""
|
||||
return self._attr_vad_sensitivity_entity_id
|
||||
|
||||
@property
|
||||
def tts_options(self) -> dict[str, Any] | None:
|
||||
"""Options passed for text-to-speech."""
|
||||
return self._attr_tts_options
|
||||
|
||||
async def async_intercept_wake_word(self) -> str | None:
|
||||
"""Intercept the next wake word from the satellite.
|
||||
|
||||
Returns the detected wake word phrase or None.
|
||||
"""
|
||||
if self._wake_word_intercept_future is not None:
|
||||
raise SatelliteBusyError("Wake word interception already in progress")
|
||||
|
||||
# Will cause next wake word to be intercepted in
|
||||
# async_accept_pipeline_from_satellite
|
||||
self._wake_word_intercept_future = asyncio.Future()
|
||||
|
||||
_LOGGER.debug("Next wake word will be intercepted: %s", self.entity_id)
|
||||
|
||||
try:
|
||||
return await self._wake_word_intercept_future
|
||||
finally:
|
||||
self._wake_word_intercept_future = None
|
||||
|
||||
async def async_internal_announce(
|
||||
self,
|
||||
message: str | None = None,
|
||||
media_id: str | None = None,
|
||||
) -> None:
|
||||
"""Play and show an announcement on the satellite.
|
||||
|
||||
If media_id is not provided, message is synthesized to
|
||||
audio with the selected pipeline.
|
||||
|
||||
If media_id is provided, it is played directly. It is possible
|
||||
to omit the message and the satellite will not show any text.
|
||||
|
||||
Calls async_announce with message and media id.
|
||||
"""
|
||||
if message is None:
|
||||
message = ""
|
||||
|
||||
if not media_id:
|
||||
# Synthesize audio and get URL
|
||||
pipeline_id = self._resolve_pipeline()
|
||||
pipeline = async_get_pipeline(self.hass, pipeline_id)
|
||||
|
||||
tts_options: dict[str, Any] = {}
|
||||
if pipeline.tts_voice is not None:
|
||||
tts_options[tts.ATTR_VOICE] = pipeline.tts_voice
|
||||
|
||||
if self.tts_options is not None:
|
||||
tts_options.update(self.tts_options)
|
||||
|
||||
media_id = tts_generate_media_source_id(
|
||||
self.hass,
|
||||
message,
|
||||
engine=pipeline.tts_engine,
|
||||
language=pipeline.tts_language,
|
||||
options=tts_options,
|
||||
)
|
||||
|
||||
if media_source.is_media_source_id(media_id):
|
||||
media = await media_source.async_resolve_media(
|
||||
self.hass,
|
||||
media_id,
|
||||
None,
|
||||
)
|
||||
media_id = media.url
|
||||
|
||||
# Resolve to full URL
|
||||
media_id = async_process_play_media_url(self.hass, media_id)
|
||||
|
||||
if self._is_announcing:
|
||||
raise SatelliteBusyError
|
||||
|
||||
self._is_announcing = True
|
||||
|
||||
try:
|
||||
# Block until announcement is finished
|
||||
await self.async_announce(message, media_id)
|
||||
finally:
|
||||
self._is_announcing = False
|
||||
|
||||
async def async_announce(self, message: str, media_id: str) -> None:
|
||||
"""Announce media on the satellite.
|
||||
|
||||
Should block until the announcement is done playing.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_accept_pipeline_from_satellite(
|
||||
self,
|
||||
audio_stream: AsyncIterable[bytes],
|
||||
start_stage: PipelineStage = PipelineStage.STT,
|
||||
end_stage: PipelineStage = PipelineStage.TTS,
|
||||
wake_word_phrase: str | None = None,
|
||||
) -> None:
|
||||
"""Triggers an Assist pipeline in Home Assistant from a satellite."""
|
||||
if self._wake_word_intercept_future and start_stage in (
|
||||
PipelineStage.WAKE_WORD,
|
||||
PipelineStage.STT,
|
||||
):
|
||||
if start_stage == PipelineStage.WAKE_WORD:
|
||||
self._wake_word_intercept_future.set_exception(
|
||||
AssistSatelliteError(
|
||||
"Only on-device wake words currently supported"
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Intercepting wake word and immediately end pipeline
|
||||
_LOGGER.debug(
|
||||
"Intercepted wake word: %s (entity_id=%s)",
|
||||
wake_word_phrase,
|
||||
self.entity_id,
|
||||
)
|
||||
|
||||
if wake_word_phrase is None:
|
||||
self._wake_word_intercept_future.set_exception(
|
||||
AssistSatelliteError("No wake word phrase provided")
|
||||
)
|
||||
else:
|
||||
self._wake_word_intercept_future.set_result(wake_word_phrase)
|
||||
self._internal_on_pipeline_event(PipelineEvent(PipelineEventType.RUN_END))
|
||||
return
|
||||
|
||||
device_id = self.registry_entry.device_id if self.registry_entry else None
|
||||
|
||||
# Refresh context if necessary
|
||||
if (
|
||||
(self._context is None)
|
||||
or (self._context_set is None)
|
||||
or ((time.time() - self._context_set) > entity.CONTEXT_RECENT_TIME_SECONDS)
|
||||
):
|
||||
self.async_set_context(Context())
|
||||
|
||||
assert self._context is not None
|
||||
|
||||
# Reset conversation id if necessary
|
||||
if (self._conversation_id_time is None) or (
|
||||
(time.monotonic() - self._conversation_id_time) > _CONVERSATION_TIMEOUT_SEC
|
||||
):
|
||||
self._conversation_id = None
|
||||
|
||||
if self._conversation_id is None:
|
||||
self._conversation_id = ulid.ulid()
|
||||
|
||||
# Update timeout
|
||||
self._conversation_id_time = time.monotonic()
|
||||
|
||||
# Set entity state based on pipeline events
|
||||
self._run_has_tts = False
|
||||
|
||||
await async_pipeline_from_audio_stream(
|
||||
self.hass,
|
||||
context=self._context,
|
||||
event_callback=self._internal_on_pipeline_event,
|
||||
stt_metadata=stt.SpeechMetadata(
|
||||
language="", # set in async_pipeline_from_audio_stream
|
||||
format=stt.AudioFormats.WAV,
|
||||
codec=stt.AudioCodecs.PCM,
|
||||
bit_rate=stt.AudioBitRates.BITRATE_16,
|
||||
sample_rate=stt.AudioSampleRates.SAMPLERATE_16000,
|
||||
channel=stt.AudioChannels.CHANNEL_MONO,
|
||||
),
|
||||
stt_stream=audio_stream,
|
||||
pipeline_id=self._resolve_pipeline(),
|
||||
conversation_id=self._conversation_id,
|
||||
device_id=device_id,
|
||||
tts_audio_output=self.tts_options,
|
||||
wake_word_phrase=wake_word_phrase,
|
||||
audio_settings=AudioSettings(
|
||||
silence_seconds=self._resolve_vad_sensitivity()
|
||||
),
|
||||
start_stage=start_stage,
|
||||
end_stage=end_stage,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def on_pipeline_event(self, event: PipelineEvent) -> None:
|
||||
"""Handle pipeline events."""
|
||||
|
||||
@callback
|
||||
def _internal_on_pipeline_event(self, event: PipelineEvent) -> None:
|
||||
"""Set state based on pipeline stage."""
|
||||
if event.type is PipelineEventType.WAKE_WORD_START:
|
||||
self._set_state(AssistSatelliteState.LISTENING_WAKE_WORD)
|
||||
elif event.type is PipelineEventType.STT_START:
|
||||
self._set_state(AssistSatelliteState.LISTENING_COMMAND)
|
||||
elif event.type is PipelineEventType.INTENT_START:
|
||||
self._set_state(AssistSatelliteState.PROCESSING)
|
||||
elif event.type is PipelineEventType.TTS_START:
|
||||
# Wait until tts_response_finished is called to return to waiting state
|
||||
self._run_has_tts = True
|
||||
self._set_state(AssistSatelliteState.RESPONDING)
|
||||
elif event.type is PipelineEventType.RUN_END:
|
||||
if not self._run_has_tts:
|
||||
self._set_state(AssistSatelliteState.LISTENING_WAKE_WORD)
|
||||
|
||||
self.on_pipeline_event(event)
|
||||
|
||||
@callback
|
||||
def _set_state(self, state: AssistSatelliteState) -> None:
|
||||
"""Set the entity's state."""
|
||||
self.__assist_satellite_state = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def tts_response_finished(self) -> None:
|
||||
"""Tell entity that the text-to-speech response has finished playing."""
|
||||
self._set_state(AssistSatelliteState.LISTENING_WAKE_WORD)
|
||||
|
||||
@callback
|
||||
def _resolve_pipeline(self) -> str | None:
|
||||
"""Resolve pipeline from select entity to id.
|
||||
|
||||
Return None to make async_get_pipeline look up the preferred pipeline.
|
||||
"""
|
||||
if not (pipeline_entity_id := self.pipeline_entity_id):
|
||||
return None
|
||||
|
||||
if (pipeline_entity_state := self.hass.states.get(pipeline_entity_id)) is None:
|
||||
raise RuntimeError("Pipeline entity not found")
|
||||
|
||||
if pipeline_entity_state.state != OPTION_PREFERRED:
|
||||
# Resolve pipeline by name
|
||||
for pipeline in async_get_pipelines(self.hass):
|
||||
if pipeline.name == pipeline_entity_state.state:
|
||||
return pipeline.id
|
||||
|
||||
return None
|
||||
|
||||
@callback
|
||||
def _resolve_vad_sensitivity(self) -> float:
|
||||
"""Resolve VAD sensitivity from select entity to enum."""
|
||||
vad_sensitivity = vad.VadSensitivity.DEFAULT
|
||||
|
||||
if vad_sensitivity_entity_id := self.vad_sensitivity_entity_id:
|
||||
if (
|
||||
vad_sensitivity_state := self.hass.states.get(vad_sensitivity_entity_id)
|
||||
) is None:
|
||||
raise RuntimeError("VAD sensitivity entity not found")
|
||||
|
||||
vad_sensitivity = vad.VadSensitivity(vad_sensitivity_state.state)
|
||||
|
||||
return vad.VadSensitivity.to_seconds(vad_sensitivity)
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Errors for assist satellite."""
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
|
||||
class AssistSatelliteError(HomeAssistantError):
|
||||
"""Base class for assist satellite errors."""
|
||||
|
||||
|
||||
class SatelliteBusyError(AssistSatelliteError):
|
||||
"""Satellite is busy and cannot handle the request."""
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:account-voice"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"announce": {
|
||||
"service": "mdi:bullhorn"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"domain": "assist_satellite",
|
||||
"name": "Assist Satellite",
|
||||
"codeowners": ["@home-assistant/core", "@synesthesiam"],
|
||||
"dependencies": ["assist_pipeline", "stt", "tts"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
announce:
|
||||
target:
|
||||
entity:
|
||||
domain: assist_satellite
|
||||
supported_features:
|
||||
- assist_satellite.AssistSatelliteEntityFeature.ANNOUNCE
|
||||
fields:
|
||||
message:
|
||||
required: false
|
||||
example: "Time to wake up!"
|
||||
selector:
|
||||
text:
|
||||
media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"title": "Assist satellite",
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "Assist satellite",
|
||||
"state": {
|
||||
"listening_wake_word": "Wake word",
|
||||
"listening_command": "Voice command",
|
||||
"responding": "Responding",
|
||||
"processing": "Processing"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"announce": {
|
||||
"name": "Announce",
|
||||
"description": "Let the satellite announce a message.",
|
||||
"fields": {
|
||||
"message": {
|
||||
"name": "Message",
|
||||
"description": "The message to announce."
|
||||
},
|
||||
"media_id": {
|
||||
"name": "Media ID",
|
||||
"description": "The media ID to announce instead of using text-to-speech."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Assist satellite Websocket API."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import AssistSatelliteEntity
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_websocket_api(hass: HomeAssistant) -> None:
|
||||
"""Register the websocket API."""
|
||||
websocket_api.async_register_command(hass, websocket_intercept_wake_word)
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "assist_satellite/intercept_wake_word",
|
||||
vol.Required("entity_id"): cv.entity_domain(DOMAIN),
|
||||
}
|
||||
)
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def websocket_intercept_wake_word(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.connection.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Intercept the next wake word from a satellite."""
|
||||
component: EntityComponent[AssistSatelliteEntity] = hass.data[DOMAIN]
|
||||
satellite = component.get_entity(msg["entity_id"])
|
||||
if satellite is None:
|
||||
connection.send_error(
|
||||
msg["id"], websocket_api.ERR_NOT_FOUND, "Entity not found"
|
||||
)
|
||||
return
|
||||
|
||||
wake_word_phrase = await satellite.async_intercept_wake_word()
|
||||
connection.send_result(msg["id"], {"wake_word_phrase": wake_word_phrase})
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import namedtuple
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
@@ -40,17 +41,23 @@ from .const import (
|
||||
PROTOCOL_HTTPS,
|
||||
PROTOCOL_TELNET,
|
||||
SENSORS_BYTES,
|
||||
SENSORS_CPU,
|
||||
SENSORS_LOAD_AVG,
|
||||
SENSORS_MEMORY,
|
||||
SENSORS_RATES,
|
||||
SENSORS_TEMPERATURES,
|
||||
SENSORS_TEMPERATURES_LEGACY,
|
||||
SENSORS_UPTIME,
|
||||
)
|
||||
|
||||
SENSORS_TYPE_BYTES = "sensors_bytes"
|
||||
SENSORS_TYPE_COUNT = "sensors_count"
|
||||
SENSORS_TYPE_CPU = "sensors_cpu"
|
||||
SENSORS_TYPE_LOAD_AVG = "sensors_load_avg"
|
||||
SENSORS_TYPE_MEMORY = "sensors_memory"
|
||||
SENSORS_TYPE_RATES = "sensors_rates"
|
||||
SENSORS_TYPE_TEMPERATURES = "sensors_temperatures"
|
||||
SENSORS_TYPE_UPTIME = "sensors_uptime"
|
||||
|
||||
WrtDevice = namedtuple("WrtDevice", ["ip", "name", "connected_to"]) # noqa: PYI024
|
||||
|
||||
@@ -346,6 +353,7 @@ class AsusWrtHttpBridge(AsusWrtBridge):
|
||||
|
||||
async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
|
||||
"""Return a dictionary of available sensors for this bridge."""
|
||||
sensors_cpu = await self._get_available_cpu_sensors()
|
||||
sensors_temperatures = await self._get_available_temperature_sensors()
|
||||
sensors_loadavg = await self._get_loadavg_sensors_availability()
|
||||
return {
|
||||
@@ -353,20 +361,49 @@ class AsusWrtHttpBridge(AsusWrtBridge):
|
||||
KEY_SENSORS: SENSORS_BYTES,
|
||||
KEY_METHOD: self._get_bytes,
|
||||
},
|
||||
SENSORS_TYPE_CPU: {
|
||||
KEY_SENSORS: sensors_cpu,
|
||||
KEY_METHOD: self._get_cpu_usage,
|
||||
},
|
||||
SENSORS_TYPE_LOAD_AVG: {
|
||||
KEY_SENSORS: sensors_loadavg,
|
||||
KEY_METHOD: self._get_load_avg,
|
||||
},
|
||||
SENSORS_TYPE_MEMORY: {
|
||||
KEY_SENSORS: SENSORS_MEMORY,
|
||||
KEY_METHOD: self._get_memory_usage,
|
||||
},
|
||||
SENSORS_TYPE_RATES: {
|
||||
KEY_SENSORS: SENSORS_RATES,
|
||||
KEY_METHOD: self._get_rates,
|
||||
},
|
||||
SENSORS_TYPE_UPTIME: {
|
||||
KEY_SENSORS: SENSORS_UPTIME,
|
||||
KEY_METHOD: self._get_uptime,
|
||||
},
|
||||
SENSORS_TYPE_TEMPERATURES: {
|
||||
KEY_SENSORS: sensors_temperatures,
|
||||
KEY_METHOD: self._get_temperatures,
|
||||
},
|
||||
}
|
||||
|
||||
async def _get_available_cpu_sensors(self) -> list[str]:
|
||||
"""Check which cpu information is available on the router."""
|
||||
try:
|
||||
available_cpu = await self._api.async_get_cpu_usage()
|
||||
available_sensors = [t for t in SENSORS_CPU if t in available_cpu]
|
||||
except AsusWrtError as exc:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Failed checking cpu sensor availability for ASUS router"
|
||||
" %s. Exception: %s"
|
||||
),
|
||||
self.host,
|
||||
exc,
|
||||
)
|
||||
return []
|
||||
return available_sensors
|
||||
|
||||
async def _get_available_temperature_sensors(self) -> list[str]:
|
||||
"""Check which temperature information is available on the router."""
|
||||
try:
|
||||
@@ -415,3 +452,25 @@ class AsusWrtHttpBridge(AsusWrtBridge):
|
||||
async def _get_temperatures(self) -> Any:
|
||||
"""Fetch temperatures information from the router."""
|
||||
return await self._api.async_get_temperatures()
|
||||
|
||||
@handle_errors_and_zip(AsusWrtError, None)
|
||||
async def _get_cpu_usage(self) -> Any:
|
||||
"""Fetch cpu information from the router."""
|
||||
return await self._api.async_get_cpu_usage()
|
||||
|
||||
@handle_errors_and_zip(AsusWrtError, None)
|
||||
async def _get_memory_usage(self) -> Any:
|
||||
"""Fetch memory information from the router."""
|
||||
return await self._api.async_get_memory_usage()
|
||||
|
||||
async def _get_uptime(self) -> dict[str, Any]:
|
||||
"""Fetch uptime from the router."""
|
||||
try:
|
||||
uptimes = await self._api.async_get_uptime()
|
||||
except AsusWrtError as exc:
|
||||
raise UpdateFailed(exc) from exc
|
||||
|
||||
last_boot = datetime.fromisoformat(uptimes["last_boot"])
|
||||
uptime = uptimes["uptime"]
|
||||
|
||||
return dict(zip(SENSORS_UPTIME, [last_boot, uptime], strict=False))
|
||||
|
||||
@@ -27,7 +27,20 @@ PROTOCOL_TELNET = "telnet"
|
||||
# Sensors
|
||||
SENSORS_BYTES = ["sensor_rx_bytes", "sensor_tx_bytes"]
|
||||
SENSORS_CONNECTED_DEVICE = ["sensor_connected_device"]
|
||||
SENSORS_CPU = [
|
||||
"cpu_total_usage",
|
||||
"cpu1_usage",
|
||||
"cpu2_usage",
|
||||
"cpu3_usage",
|
||||
"cpu4_usage",
|
||||
"cpu5_usage",
|
||||
"cpu6_usage",
|
||||
"cpu7_usage",
|
||||
"cpu8_usage",
|
||||
]
|
||||
SENSORS_LOAD_AVG = ["sensor_load_avg1", "sensor_load_avg5", "sensor_load_avg15"]
|
||||
SENSORS_MEMORY = ["mem_usage_perc", "mem_free", "mem_used"]
|
||||
SENSORS_RATES = ["sensor_rx_rates", "sensor_tx_rates"]
|
||||
SENSORS_TEMPERATURES_LEGACY = ["2.4GHz", "5.0GHz", "CPU"]
|
||||
SENSORS_TEMPERATURES = [*SENSORS_TEMPERATURES_LEGACY, "5.0GHz_2", "6.0GHz"]
|
||||
SENSORS_UPTIME = ["sensor_last_boot", "sensor_uptime"]
|
||||
|
||||
@@ -24,6 +24,21 @@
|
||||
},
|
||||
"load_avg_15m": {
|
||||
"default": "mdi:cpu-32-bit"
|
||||
},
|
||||
"cpu_usage": {
|
||||
"default": "mdi:cpu-32-bit"
|
||||
},
|
||||
"cpu_core_usage": {
|
||||
"default": "mdi:cpu-32-bit"
|
||||
},
|
||||
"memory_usage": {
|
||||
"default": "mdi:memory"
|
||||
},
|
||||
"memory_free": {
|
||||
"default": "mdi:memory"
|
||||
},
|
||||
"memory_used": {
|
||||
"default": "mdi:memory"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfDataRate,
|
||||
UnitOfInformation,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -30,9 +32,12 @@ from .const import (
|
||||
KEY_SENSORS,
|
||||
SENSORS_BYTES,
|
||||
SENSORS_CONNECTED_DEVICE,
|
||||
SENSORS_CPU,
|
||||
SENSORS_LOAD_AVG,
|
||||
SENSORS_MEMORY,
|
||||
SENSORS_RATES,
|
||||
SENSORS_TEMPERATURES,
|
||||
SENSORS_UPTIME,
|
||||
)
|
||||
from .router import AsusWrtRouter
|
||||
|
||||
@@ -46,6 +51,19 @@ class AsusWrtSensorEntityDescription(SensorEntityDescription):
|
||||
|
||||
UNIT_DEVICES = "Devices"
|
||||
|
||||
CPU_CORE_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = tuple(
|
||||
AsusWrtSensorEntityDescription(
|
||||
key=sens_key,
|
||||
translation_key="cpu_core_usage",
|
||||
translation_placeholders={"core_id": str(core_id)},
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
suggested_display_precision=1,
|
||||
)
|
||||
for core_id, sens_key in enumerate(SENSORS_CPU[1:], start=1)
|
||||
)
|
||||
CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
|
||||
AsusWrtSensorEntityDescription(
|
||||
key=SENSORS_CONNECTED_DEVICE[0],
|
||||
@@ -167,6 +185,61 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
AsusWrtSensorEntityDescription(
|
||||
key=SENSORS_MEMORY[0],
|
||||
translation_key="memory_usage",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
AsusWrtSensorEntityDescription(
|
||||
key=SENSORS_MEMORY[1],
|
||||
translation_key="memory_free",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement=UnitOfInformation.MEGABYTES,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
suggested_display_precision=2,
|
||||
factor=1024,
|
||||
),
|
||||
AsusWrtSensorEntityDescription(
|
||||
key=SENSORS_MEMORY[2],
|
||||
translation_key="memory_used",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement=UnitOfInformation.MEGABYTES,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
suggested_display_precision=2,
|
||||
factor=1024,
|
||||
),
|
||||
AsusWrtSensorEntityDescription(
|
||||
key=SENSORS_UPTIME[0],
|
||||
translation_key="last_boot",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
AsusWrtSensorEntityDescription(
|
||||
key=SENSORS_UPTIME[1],
|
||||
translation_key="uptime",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
AsusWrtSensorEntityDescription(
|
||||
key=SENSORS_CPU[0],
|
||||
translation_key="cpu_usage",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
*CPU_CORE_SENSORS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -88,6 +88,27 @@
|
||||
},
|
||||
"6ghz_temperature": {
|
||||
"name": "6GHz Temperature"
|
||||
},
|
||||
"cpu_usage": {
|
||||
"name": "CPU usage"
|
||||
},
|
||||
"cpu_core_usage": {
|
||||
"name": "CPU core {core_id} usage"
|
||||
},
|
||||
"memory_usage": {
|
||||
"name": "Memory usage"
|
||||
},
|
||||
"memory_free": {
|
||||
"name": "Memory free"
|
||||
},
|
||||
"memory_used": {
|
||||
"name": "Memory used"
|
||||
},
|
||||
"last_boot": {
|
||||
"name": "Last boot"
|
||||
},
|
||||
"uptime": {
|
||||
"name": "Uptime"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -991,15 +991,15 @@ async def _create_automation_entities(
|
||||
|
||||
# Add trigger variables to variables
|
||||
variables = None
|
||||
if CONF_TRIGGER_VARIABLES in config_block:
|
||||
if CONF_TRIGGER_VARIABLES in config_block and CONF_VARIABLES in config_block:
|
||||
variables = ScriptVariables(
|
||||
dict(config_block[CONF_TRIGGER_VARIABLES].as_dict())
|
||||
)
|
||||
if CONF_VARIABLES in config_block:
|
||||
if variables:
|
||||
variables.variables.update(config_block[CONF_VARIABLES].as_dict())
|
||||
else:
|
||||
variables = config_block[CONF_VARIABLES]
|
||||
variables.variables.update(config_block[CONF_VARIABLES].as_dict())
|
||||
elif CONF_TRIGGER_VARIABLES in config_block:
|
||||
variables = config_block[CONF_TRIGGER_VARIABLES]
|
||||
elif CONF_VARIABLES in config_block:
|
||||
variables = config_block[CONF_VARIABLES]
|
||||
|
||||
entity = AutomationEntity(
|
||||
automation_id,
|
||||
|
||||
@@ -293,6 +293,7 @@ class AwairSensor(CoordinatorEntity[AwairDataUpdateCoordinator], SensorEntity):
|
||||
identifiers={(DOMAIN, self._device.uuid)},
|
||||
manufacturer="Awair",
|
||||
model=self._device.model,
|
||||
model_id=self._device.device_type,
|
||||
name=(
|
||||
self._device.name
|
||||
or cast(ConfigEntry, self.coordinator.config_entry).title
|
||||
|
||||
@@ -6,8 +6,14 @@ import logging
|
||||
from typing import Final
|
||||
|
||||
from aioazuredevops.client import DevOpsClient
|
||||
from aioazuredevops.helper import (
|
||||
WorkItemTypeAndState,
|
||||
work_item_types_states_filter,
|
||||
work_items_by_type_and_state,
|
||||
)
|
||||
from aioazuredevops.models.build import Build
|
||||
from aioazuredevops.models.core import Project
|
||||
from aioazuredevops.models.work_item_type import Category
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -20,6 +26,7 @@ from .const import CONF_ORG, DOMAIN
|
||||
from .data import AzureDevOpsData
|
||||
|
||||
BUILDS_QUERY: Final = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1"
|
||||
IGNORED_CATEGORIES: Final[list[Category]] = [Category.COMPLETED, Category.REMOVED]
|
||||
|
||||
|
||||
def ado_exception_none_handler(func: Callable) -> Callable:
|
||||
@@ -105,13 +112,60 @@ class AzureDevOpsDataUpdateCoordinator(DataUpdateCoordinator[AzureDevOpsData]):
|
||||
BUILDS_QUERY,
|
||||
)
|
||||
|
||||
@ado_exception_none_handler
|
||||
async def _get_work_items(
|
||||
self, project_name: str
|
||||
) -> list[WorkItemTypeAndState] | None:
|
||||
"""Get the work items."""
|
||||
|
||||
if (
|
||||
work_item_types := await self.client.get_work_item_types(
|
||||
self.organization,
|
||||
project_name,
|
||||
)
|
||||
) is None:
|
||||
# If no work item types are returned, return an empty list
|
||||
return []
|
||||
|
||||
if (
|
||||
work_item_ids := await self.client.get_work_item_ids(
|
||||
self.organization,
|
||||
project_name,
|
||||
# Filter out completed and removed work items so we only get active work items
|
||||
states=work_item_types_states_filter(
|
||||
work_item_types,
|
||||
ignored_categories=IGNORED_CATEGORIES,
|
||||
),
|
||||
)
|
||||
) is None:
|
||||
# If no work item ids are returned, return an empty list
|
||||
return []
|
||||
|
||||
if (
|
||||
work_items := await self.client.get_work_items(
|
||||
self.organization,
|
||||
project_name,
|
||||
work_item_ids,
|
||||
)
|
||||
) is None:
|
||||
# If no work items are returned, return an empty list
|
||||
return []
|
||||
|
||||
return work_items_by_type_and_state(
|
||||
work_item_types,
|
||||
work_items,
|
||||
ignored_categories=IGNORED_CATEGORIES,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> AzureDevOpsData:
|
||||
"""Fetch data from Azure DevOps."""
|
||||
# Get the builds from the project
|
||||
builds = await self._get_builds(self.project.name)
|
||||
work_items = await self._get_work_items(self.project.name)
|
||||
|
||||
return AzureDevOpsData(
|
||||
organization=self.organization,
|
||||
project=self.project,
|
||||
builds=builds,
|
||||
work_items=work_items,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aioazuredevops.helper import WorkItemTypeAndState
|
||||
from aioazuredevops.models.build import Build
|
||||
from aioazuredevops.models.core import Project
|
||||
|
||||
@@ -13,3 +14,4 @@ class AzureDevOpsData:
|
||||
organization: str
|
||||
project: Project
|
||||
builds: list[Build]
|
||||
work_items: list[WorkItemTypeAndState]
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"sensor": {
|
||||
"latest_build": {
|
||||
"default": "mdi:pipe"
|
||||
},
|
||||
"work_item_count": {
|
||||
"default": "mdi:ticket"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from datetime import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioazuredevops.helper import WorkItemState, WorkItemTypeAndState
|
||||
from aioazuredevops.models.build import Build
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -29,12 +30,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AzureDevOpsBuildSensorEntityDescription(SensorEntityDescription):
|
||||
"""Class describing Azure DevOps base build sensor entities."""
|
||||
"""Class describing Azure DevOps build sensor entities."""
|
||||
|
||||
attr_fn: Callable[[Build], dict[str, Any] | None] = lambda _: None
|
||||
value_fn: Callable[[Build], datetime | StateType]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AzureDevOpsWorkItemSensorEntityDescription(SensorEntityDescription):
|
||||
"""Class describing Azure DevOps work item sensor entities."""
|
||||
|
||||
value_fn: Callable[[WorkItemState], datetime | StateType]
|
||||
|
||||
|
||||
BASE_BUILD_SENSOR_DESCRIPTIONS: tuple[AzureDevOpsBuildSensorEntityDescription, ...] = (
|
||||
# Attributes are deprecated in 2024.7 and can be removed in 2025.1
|
||||
AzureDevOpsBuildSensorEntityDescription(
|
||||
@@ -116,6 +124,16 @@ BASE_BUILD_SENSOR_DESCRIPTIONS: tuple[AzureDevOpsBuildSensorEntityDescription, .
|
||||
),
|
||||
)
|
||||
|
||||
BASE_WORK_ITEM_SENSOR_DESCRIPTIONS: tuple[
|
||||
AzureDevOpsWorkItemSensorEntityDescription, ...
|
||||
] = (
|
||||
AzureDevOpsWorkItemSensorEntityDescription(
|
||||
key="work_item_count",
|
||||
translation_key="work_item_count",
|
||||
value_fn=lambda work_item_state: len(work_item_state.work_items),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def parse_datetime(value: str | None) -> datetime | None:
|
||||
"""Parse datetime string."""
|
||||
@@ -134,7 +152,7 @@ async def async_setup_entry(
|
||||
coordinator = entry.runtime_data
|
||||
initial_builds: list[Build] = coordinator.data.builds
|
||||
|
||||
async_add_entities(
|
||||
entities: list[SensorEntity] = [
|
||||
AzureDevOpsBuildSensor(
|
||||
coordinator,
|
||||
description,
|
||||
@@ -143,8 +161,22 @@ async def async_setup_entry(
|
||||
for description in BASE_BUILD_SENSOR_DESCRIPTIONS
|
||||
for key, build in enumerate(initial_builds)
|
||||
if build.project and build.definition
|
||||
]
|
||||
|
||||
entities.extend(
|
||||
AzureDevOpsWorkItemSensor(
|
||||
coordinator,
|
||||
description,
|
||||
key,
|
||||
state_key,
|
||||
)
|
||||
for description in BASE_WORK_ITEM_SENSOR_DESCRIPTIONS
|
||||
for key, work_item_type_state in enumerate(coordinator.data.work_items)
|
||||
for state_key, _ in enumerate(work_item_type_state.state_items)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AzureDevOpsBuildSensor(AzureDevOpsEntity, SensorEntity):
|
||||
"""Define a Azure DevOps build sensor."""
|
||||
@@ -162,8 +194,8 @@ class AzureDevOpsBuildSensor(AzureDevOpsEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
self.item_key = item_key
|
||||
self._attr_unique_id = (
|
||||
f"{self.coordinator.data.organization}_"
|
||||
f"{self.build.project.id}_"
|
||||
f"{coordinator.data.organization}_"
|
||||
f"{coordinator.data.project.id}_"
|
||||
f"{self.build.definition.build_id}_"
|
||||
f"{description.key}"
|
||||
)
|
||||
@@ -185,3 +217,48 @@ class AzureDevOpsBuildSensor(AzureDevOpsEntity, SensorEntity):
|
||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
"""Return the state attributes of the entity."""
|
||||
return self.entity_description.attr_fn(self.build)
|
||||
|
||||
|
||||
class AzureDevOpsWorkItemSensor(AzureDevOpsEntity, SensorEntity):
|
||||
"""Define a Azure DevOps work item sensor."""
|
||||
|
||||
entity_description: AzureDevOpsWorkItemSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AzureDevOpsDataUpdateCoordinator,
|
||||
description: AzureDevOpsWorkItemSensorEntityDescription,
|
||||
wits_key: int,
|
||||
state_key: int,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self.wits_key = wits_key
|
||||
self.state_key = state_key
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.data.organization}_"
|
||||
f"{coordinator.data.project.id}_"
|
||||
f"{self.work_item_type.name}_"
|
||||
f"{self.work_item_state.name}_"
|
||||
f"{description.key}"
|
||||
)
|
||||
self._attr_translation_placeholders = {
|
||||
"item_type": self.work_item_type.name,
|
||||
"item_state": self.work_item_state.name,
|
||||
}
|
||||
|
||||
@property
|
||||
def work_item_type(self) -> WorkItemTypeAndState:
|
||||
"""Return the work item."""
|
||||
return self.coordinator.data.work_items[self.wits_key]
|
||||
|
||||
@property
|
||||
def work_item_state(self) -> WorkItemState:
|
||||
"""Return the work item state."""
|
||||
return self.work_item_type.state_items[self.state_key]
|
||||
|
||||
@property
|
||||
def native_value(self) -> datetime | StateType:
|
||||
"""Return the state."""
|
||||
return self.entity_description.value_fn(self.work_item_state)
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
},
|
||||
"url": {
|
||||
"name": "{definition_name} latest build url"
|
||||
},
|
||||
"work_item_count": {
|
||||
"name": "{item_type} {item_state} work items"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,7 +17,6 @@ from homeassistant.const import CONF_HOST, CONF_MODEL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from .const import DOMAIN
|
||||
from .websocket import BangOlufsenWebsocket
|
||||
@@ -49,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
model=entry.data[CONF_MODEL],
|
||||
)
|
||||
|
||||
client = MozartClient(host=entry.data[CONF_HOST], ssl_context=get_default_context())
|
||||
client = MozartClient(host=entry.data[CONF_HOST])
|
||||
|
||||
# Check API and WebSocket connection
|
||||
try:
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_MODEL
|
||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from .const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
@@ -88,9 +87,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors={"base": _exception_map[type(error)]},
|
||||
)
|
||||
|
||||
self._client = MozartClient(
|
||||
host=self._host, ssl_context=get_default_context()
|
||||
)
|
||||
self._client = MozartClient(self._host)
|
||||
|
||||
# Try to get information from Beolink self method.
|
||||
async with self._client:
|
||||
@@ -139,7 +136,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="ipv6_address")
|
||||
|
||||
# Check connection to ensure valid address is received
|
||||
self._client = MozartClient(self._host, ssl_context=get_default_context())
|
||||
self._client = MozartClient(self._host)
|
||||
|
||||
async with self._client:
|
||||
try:
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["mozart-api==3.4.1.8.8"],
|
||||
"requirements": ["mozart-api==3.4.1.8.6"],
|
||||
"zeroconf": ["_bangolufsen._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/blebox",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["blebox_uniapi"],
|
||||
"requirements": ["blebox-uniapi==2.4.2"],
|
||||
"requirements": ["blebox-uniapi==2.5.0"],
|
||||
"zeroconf": ["_bbxsrv._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import aiohttp
|
||||
from pyblu import Player, SyncStatus
|
||||
from pyblu.errors import PlayerUnreachableError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
@@ -22,14 +22,14 @@ PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
@dataclass
|
||||
class BluesoundData:
|
||||
class BluesoundRuntimeData:
|
||||
"""Bluesound data class."""
|
||||
|
||||
player: Player
|
||||
sync_status: SyncStatus
|
||||
|
||||
|
||||
type BluesoundConfigEntry = ConfigEntry[BluesoundData]
|
||||
type BluesoundConfigEntry = ConfigEntry[BluesoundRuntimeData]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@@ -51,14 +51,10 @@ async def async_setup_entry(
|
||||
async with Player(host, port, session=session, default_timeout=10) as player:
|
||||
try:
|
||||
sync_status = await player.sync_status(timeout=1)
|
||||
except TimeoutError as ex:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Timeout while connecting to {host}:{port}"
|
||||
) from ex
|
||||
except aiohttp.ClientError as ex:
|
||||
except PlayerUnreachableError as ex:
|
||||
raise ConfigEntryNotReady(f"Error connecting to {host}:{port}") from ex
|
||||
|
||||
config_entry.runtime_data = BluesoundData(player, sync_status)
|
||||
config_entry.runtime_data = BluesoundRuntimeData(player, sync_status)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from pyblu import Player, SyncStatus
|
||||
from pyblu.errors import PlayerUnreachableError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
@@ -43,7 +43,7 @@ class BluesoundConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) as player:
|
||||
try:
|
||||
sync_status = await player.sync_status(timeout=1)
|
||||
except (TimeoutError, aiohttp.ClientError):
|
||||
except PlayerUnreachableError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(
|
||||
@@ -79,7 +79,7 @@ class BluesoundConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) as player:
|
||||
try:
|
||||
sync_status = await player.sync_status(timeout=1)
|
||||
except (TimeoutError, aiohttp.ClientError):
|
||||
except PlayerUnreachableError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
await self.async_set_unique_id(
|
||||
@@ -105,7 +105,7 @@ class BluesoundConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
discovery_info.host, self._port, session=session
|
||||
) as player:
|
||||
sync_status = await player.sync_status(timeout=1)
|
||||
except (TimeoutError, aiohttp.ClientError):
|
||||
except PlayerUnreachableError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
await self.async_set_unique_id(format_unique_id(sync_status.mac, self._port))
|
||||
@@ -127,7 +127,9 @@ class BluesoundConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(self, user_input=None) -> ConfigFlowResult:
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm the zeroconf setup."""
|
||||
assert self._sync_status is not None
|
||||
assert self._host is not None
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyblu==0.4.0"],
|
||||
"requirements": ["pyblu==1.0.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_musc._tcp.local."
|
||||
|
||||
@@ -9,8 +9,8 @@ from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
from pyblu import Input, Player, Preset, Status, SyncStatus
|
||||
from pyblu.errors import PlayerUnreachableError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import media_source
|
||||
@@ -239,7 +239,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
self.port = port
|
||||
self._polling_task: Task[None] | None = None # The actual polling task.
|
||||
self._id = sync_status.id
|
||||
self._last_status_update = None
|
||||
self._last_status_update: datetime | None = None
|
||||
self._sync_status = sync_status
|
||||
self._status: Status | None = None
|
||||
self._inputs: list[Input] = []
|
||||
@@ -247,7 +247,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
self._muted = False
|
||||
self._master: BluesoundPlayer | None = None
|
||||
self._is_master = False
|
||||
self._group_name = None
|
||||
self._group_name: str | None = None
|
||||
self._group_list: list[str] = []
|
||||
self._bluesound_device_name = sync_status.name
|
||||
self._player = player
|
||||
@@ -273,14 +273,6 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
via_device=(DOMAIN, format_mac(sync_status.mac)),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _try_get_index(string, search_string):
|
||||
"""Get the index."""
|
||||
try:
|
||||
return string.index(search_string)
|
||||
except ValueError:
|
||||
return -1
|
||||
|
||||
async def force_update_sync_status(self) -> bool:
|
||||
"""Update the internal status."""
|
||||
sync_status = await self._player.sync_status()
|
||||
@@ -309,12 +301,12 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
return True
|
||||
|
||||
async def _poll_loop(self):
|
||||
async def _poll_loop(self) -> None:
|
||||
"""Loop which polls the status of the player."""
|
||||
while True:
|
||||
try:
|
||||
await self.async_update_status()
|
||||
except (TimeoutError, ClientError):
|
||||
except PlayerUnreachableError:
|
||||
_LOGGER.error(
|
||||
"Node %s:%s is offline, retrying later", self.host, self.port
|
||||
)
|
||||
@@ -324,9 +316,9 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
"Stopping the polling of node %s:%s", self.host, self.port
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
except: # noqa: E722 - this loop should never stop
|
||||
_LOGGER.exception(
|
||||
"Unexpected error in %s:%s, retrying later", self.host, self.port
|
||||
"Unexpected error for %s:%s, retrying later", self.host, self.port
|
||||
)
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
||||
|
||||
@@ -356,12 +348,12 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
if not self.available:
|
||||
return
|
||||
|
||||
with suppress(TimeoutError):
|
||||
with suppress(PlayerUnreachableError):
|
||||
await self.async_update_sync_status()
|
||||
await self.async_update_presets()
|
||||
await self.async_update_captures()
|
||||
|
||||
async def async_update_status(self):
|
||||
async def async_update_status(self) -> None:
|
||||
"""Use the poll session to always get the status of the player."""
|
||||
etag = None
|
||||
if self._status is not None:
|
||||
@@ -394,11 +386,11 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
# the device is playing. This would solve a lot of
|
||||
# problems. This change will be done when the
|
||||
# communication is moved to a separate library
|
||||
with suppress(TimeoutError):
|
||||
with suppress(PlayerUnreachableError):
|
||||
await self.force_update_sync_status()
|
||||
|
||||
self.async_write_ha_state()
|
||||
except (TimeoutError, ClientError):
|
||||
except PlayerUnreachableError:
|
||||
self._attr_available = False
|
||||
self._last_status_update = None
|
||||
self._status = None
|
||||
@@ -409,7 +401,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
)
|
||||
raise
|
||||
|
||||
async def async_trigger_sync_on_all(self):
|
||||
async def async_trigger_sync_on_all(self) -> None:
|
||||
"""Trigger sync status update on all devices."""
|
||||
_LOGGER.debug("Trigger sync status on all devices")
|
||||
|
||||
@@ -417,7 +409,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
await player.force_update_sync_status()
|
||||
|
||||
@Throttle(SYNC_STATUS_INTERVAL)
|
||||
async def async_update_sync_status(self):
|
||||
async def async_update_sync_status(self) -> None:
|
||||
"""Update sync status."""
|
||||
await self.force_update_sync_status()
|
||||
|
||||
@@ -506,8 +498,6 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
return None
|
||||
|
||||
position = self._status.seconds
|
||||
if position is None:
|
||||
return None
|
||||
|
||||
if mediastate == MediaPlayerState.PLAYING:
|
||||
position += (dt_util.utcnow() - self._last_status_update).total_seconds()
|
||||
@@ -524,7 +514,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
if duration is None:
|
||||
return None
|
||||
|
||||
return duration
|
||||
return int(duration)
|
||||
|
||||
@property
|
||||
def media_position_updated_at(self) -> datetime | None:
|
||||
@@ -660,7 +650,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
return shuffle
|
||||
|
||||
async def async_join(self, master):
|
||||
async def async_join(self, master: str) -> None:
|
||||
"""Join the player to a group."""
|
||||
master_device = [
|
||||
device
|
||||
@@ -711,7 +701,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
if entity.bluesound_device_name in device_group
|
||||
]
|
||||
|
||||
async def async_unjoin(self):
|
||||
async def async_unjoin(self) -> None:
|
||||
"""Unjoin the player from a group."""
|
||||
if self._master is None:
|
||||
return
|
||||
@@ -719,11 +709,11 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
_LOGGER.debug("Trying to unjoin player: %s", self.id)
|
||||
await self._master.async_remove_slave(self)
|
||||
|
||||
async def async_add_slave(self, slave_device: BluesoundPlayer):
|
||||
async def async_add_slave(self, slave_device: BluesoundPlayer) -> None:
|
||||
"""Add slave to master."""
|
||||
await self._player.add_slave(slave_device.host, slave_device.port)
|
||||
|
||||
async def async_remove_slave(self, slave_device: BluesoundPlayer):
|
||||
async def async_remove_slave(self, slave_device: BluesoundPlayer) -> None:
|
||||
"""Remove slave to master."""
|
||||
await self._player.remove_slave(slave_device.host, slave_device.port)
|
||||
|
||||
@@ -731,7 +721,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
"""Increase sleep time on player."""
|
||||
return await self._player.sleep_timer()
|
||||
|
||||
async def async_clear_timer(self):
|
||||
async def async_clear_timer(self) -> None:
|
||||
"""Clear sleep timer on player."""
|
||||
sleep = 1
|
||||
while sleep > 0:
|
||||
@@ -755,6 +745,9 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
if preset.name == source:
|
||||
url = preset.url
|
||||
|
||||
if url is None:
|
||||
raise ServiceValidationError(f"Source {source} not found")
|
||||
|
||||
await self._player.play_url(url)
|
||||
|
||||
async def async_clear_playlist(self) -> None:
|
||||
@@ -826,20 +819,20 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up the media player."""
|
||||
if self.volume_level is None:
|
||||
return None
|
||||
return
|
||||
|
||||
new_volume = self.volume_level + 0.01
|
||||
new_volume = min(1, new_volume)
|
||||
return await self.async_set_volume_level(new_volume)
|
||||
await self.async_set_volume_level(new_volume)
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down the media player."""
|
||||
if self.volume_level is None:
|
||||
return None
|
||||
return
|
||||
|
||||
new_volume = self.volume_level - 0.01
|
||||
new_volume = max(0, new_volume)
|
||||
return await self.async_set_volume_level(new_volume)
|
||||
await self.async_set_volume_level(new_volume)
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Send volume_up command to media player."""
|
||||
|
||||
@@ -33,11 +33,13 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_EMAIL): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.EMAIL,
|
||||
autocomplete="email",
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD,
|
||||
autocomplete="current-password",
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Base entity for the Bring! integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import BringData, BringDataUpdateCoordinator
|
||||
|
||||
|
||||
class BringBaseEntity(CoordinatorEntity[BringDataUpdateCoordinator]):
|
||||
"""Bring base entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: BringDataUpdateCoordinator,
|
||||
bring_list: BringData,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._list_uuid = bring_list["listUuid"]
|
||||
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{self._list_uuid}"
|
||||
|
||||
self.device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
name=bring_list["name"],
|
||||
identifiers={
|
||||
(DOMAIN, f"{coordinator.config_entry.unique_id}_{self._list_uuid}")
|
||||
},
|
||||
manufacturer="Bring! Labs AG",
|
||||
model="Bring! Grocery Shopping List",
|
||||
configuration_url=f"https://web.getbring.com/app/lists/{list(self.coordinator.data.keys()).index(self._list_uuid)}",
|
||||
)
|
||||
@@ -23,7 +23,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import BringConfigEntry
|
||||
from .const import (
|
||||
@@ -32,7 +31,8 @@ from .const import (
|
||||
DOMAIN,
|
||||
SERVICE_PUSH_NOTIFICATION,
|
||||
)
|
||||
from .coordinator import BringData, BringDataUpdateCoordinator
|
||||
from .coordinator import BringData
|
||||
from .entity import BringBaseEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -43,16 +43,10 @@ async def async_setup_entry(
|
||||
"""Set up the sensor from a config entry created in the integrations UI."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
unique_id = config_entry.unique_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert unique_id
|
||||
|
||||
async_add_entities(
|
||||
BringTodoListEntity(
|
||||
coordinator,
|
||||
bring_list=bring_list,
|
||||
unique_id=unique_id,
|
||||
)
|
||||
for bring_list in coordinator.data.values()
|
||||
)
|
||||
@@ -71,13 +65,11 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class BringTodoListEntity(
|
||||
CoordinatorEntity[BringDataUpdateCoordinator], TodoListEntity
|
||||
):
|
||||
class BringTodoListEntity(BringBaseEntity, TodoListEntity):
|
||||
"""A To-do List representation of the Bring! Shopping List."""
|
||||
|
||||
_attr_translation_key = "shopping_list"
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_supported_features = (
|
||||
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
@@ -85,18 +77,6 @@ class BringTodoListEntity(
|
||||
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: BringDataUpdateCoordinator,
|
||||
bring_list: BringData,
|
||||
unique_id: str,
|
||||
) -> None:
|
||||
"""Initialize BringTodoListEntity."""
|
||||
super().__init__(coordinator)
|
||||
self._list_uuid = bring_list["listUuid"]
|
||||
self._attr_name = bring_list["name"]
|
||||
self._attr_unique_id = f"{unique_id}_{self._list_uuid}"
|
||||
|
||||
@property
|
||||
def todo_items(self) -> list[TodoItem]:
|
||||
"""Return the todo items."""
|
||||
|
||||
@@ -5,7 +5,7 @@ import errno
|
||||
from functools import partial
|
||||
import logging
|
||||
import socket
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
import broadlink as blk
|
||||
from broadlink.exceptions import (
|
||||
@@ -37,9 +37,7 @@ class BroadlinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the Broadlink flow."""
|
||||
self.device: blk.Device | None = None
|
||||
device: blk.Device
|
||||
|
||||
async def async_set_device(
|
||||
self, device: blk.Device, raise_on_progress: bool = True
|
||||
@@ -131,8 +129,6 @@ class BroadlinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
return await self.async_step_auth()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert self.device
|
||||
if device.mac == self.device.mac:
|
||||
await self.async_set_device(device, raise_on_progress=False)
|
||||
return await self.async_step_auth()
|
||||
@@ -158,10 +154,10 @@ class BroadlinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_auth(self):
|
||||
async def async_step_auth(self) -> ConfigFlowResult:
|
||||
"""Authenticate to the device."""
|
||||
device = self.device
|
||||
errors = {}
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(device.auth)
|
||||
@@ -211,7 +207,11 @@ class BroadlinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
return self.async_show_form(step_id="auth", errors=errors)
|
||||
|
||||
async def async_step_reset(self, user_input=None, errors=None):
|
||||
async def async_step_reset(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
errors: dict[str, str] | None = None,
|
||||
) -> ConfigFlowResult:
|
||||
"""Guide the user to unlock the device manually.
|
||||
|
||||
We are unable to authenticate because the device is locked.
|
||||
@@ -234,7 +234,9 @@ class BroadlinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
{CONF_HOST: device.host[0], CONF_TIMEOUT: device.timeout}
|
||||
)
|
||||
|
||||
async def async_step_unlock(self, user_input=None):
|
||||
async def async_step_unlock(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Unlock the device.
|
||||
|
||||
The authentication succeeded, but the device is locked.
|
||||
@@ -288,10 +290,12 @@ class BroadlinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_finish(self, user_input=None):
|
||||
async def async_step_finish(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Choose a name for the device and create config entry."""
|
||||
device = self.device
|
||||
errors = {}
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
# Abort reauthentication flow.
|
||||
self._abort_if_unique_id_configured(
|
||||
|
||||
@@ -126,15 +126,14 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set preset mode."""
|
||||
# only allow preset mode when hvac mode is auto
|
||||
if self.hvac_mode == HVACMode.AUTO:
|
||||
await self.async_set_data(preset_mode=preset_mode)
|
||||
else:
|
||||
if self.hvac_mode != HVACMode.AUTO and preset_mode != PRESET_NONE:
|
||||
raise ServiceValidationError(
|
||||
"Preset mode can only be set when HVAC mode is set to 'auto'",
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_preset_mode_error",
|
||||
translation_placeholders={"preset_mode": preset_mode},
|
||||
)
|
||||
await self.async_set_data(preset_mode=preset_mode)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperatures."""
|
||||
@@ -148,11 +147,11 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
||||
if ATTR_HVAC_MODE in kwargs:
|
||||
data[ATTR_HVAC_MODE] = kwargs[ATTR_HVAC_MODE]
|
||||
if ATTR_PRESET_MODE in kwargs:
|
||||
# If preset mode is None, set hvac to auto
|
||||
if kwargs[ATTR_PRESET_MODE] == PRESET_NONE:
|
||||
data[ATTR_HVAC_MODE] = HVACMode.AUTO
|
||||
else:
|
||||
data[ATTR_HVAC_MODE] = kwargs[ATTR_PRESET_MODE]
|
||||
if kwargs[ATTR_PRESET_MODE] == PRESET_ECO:
|
||||
data[ATTR_HVAC_MODE] = PRESET_ECO
|
||||
elif kwargs[ATTR_PRESET_MODE] == PRESET_NONE:
|
||||
data[ATTR_HVAC_MODE] = PRESET_NONE
|
||||
|
||||
try:
|
||||
await self.coordinator.client.thermostat(**data)
|
||||
except BSBLANError as err:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -34,7 +34,7 @@ from .const import (
|
||||
EVENT_TYPE,
|
||||
)
|
||||
|
||||
TRIGGERS_BY_EVENT_CLASS = {
|
||||
EVENT_TYPES_BY_EVENT_CLASS = {
|
||||
EVENT_CLASS_BUTTON: {
|
||||
"press",
|
||||
"double_press",
|
||||
@@ -51,6 +51,38 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def get_event_classes_by_device_id(hass: HomeAssistant, device_id: str) -> list[str]:
|
||||
"""Get the supported event classes for a device.
|
||||
|
||||
Events for BTHome BLE devices are dynamically discovered
|
||||
and stored in the device config entry when they are first seen.
|
||||
"""
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get(device_id)
|
||||
if TYPE_CHECKING:
|
||||
assert device is not None
|
||||
|
||||
config_entries = [
|
||||
hass.config_entries.async_get_entry(entry_id)
|
||||
for entry_id in device.config_entries
|
||||
]
|
||||
bthome_config_entry = next(
|
||||
entry for entry in config_entries if entry and entry.domain == DOMAIN
|
||||
)
|
||||
return bthome_config_entry.data.get(CONF_DISCOVERED_EVENT_CLASSES, [])
|
||||
|
||||
|
||||
def get_event_types_by_event_class(event_class: str) -> set[str]:
|
||||
"""Get the supported event types for an event class.
|
||||
|
||||
If the device has multiple buttons they will have
|
||||
event classes like button_1 button_2, button_3, etc
|
||||
but if there is only one button then it will be
|
||||
button without a number postfix.
|
||||
"""
|
||||
return EVENT_TYPES_BY_EVENT_CLASS.get(event_class.split("_")[0], set())
|
||||
|
||||
|
||||
async def async_validate_trigger_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
@@ -58,31 +90,17 @@ async def async_validate_trigger_config(
|
||||
config = TRIGGER_SCHEMA(config)
|
||||
event_class = config[CONF_TYPE]
|
||||
event_type = config[CONF_SUBTYPE]
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get(config[CONF_DEVICE_ID])
|
||||
assert device is not None
|
||||
config_entries = [
|
||||
hass.config_entries.async_get_entry(entry_id)
|
||||
for entry_id in device.config_entries
|
||||
]
|
||||
bthome_config_entry = next(
|
||||
iter(entry for entry in config_entries if entry and entry.domain == DOMAIN)
|
||||
)
|
||||
event_classes: list[str] = bthome_config_entry.data.get(
|
||||
CONF_DISCOVERED_EVENT_CLASSES, []
|
||||
)
|
||||
device_id = config[CONF_DEVICE_ID]
|
||||
event_classes = get_event_classes_by_device_id(hass, device_id)
|
||||
|
||||
if event_class not in event_classes:
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"BTHome trigger {event_class} is not valid for device "
|
||||
f"{device} ({config[CONF_DEVICE_ID]})"
|
||||
f"BTHome trigger {event_class} is not valid for device_id '{device_id}'"
|
||||
)
|
||||
|
||||
if event_type not in TRIGGERS_BY_EVENT_CLASS.get(event_class.split("_")[0], ()):
|
||||
if event_type not in get_event_types_by_event_class(event_class):
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"BTHome trigger {event_type} is not valid for device "
|
||||
f"{device} ({config[CONF_DEVICE_ID]})"
|
||||
f"BTHome trigger {event_type} is not valid for device_id '{device_id}'"
|
||||
)
|
||||
|
||||
return config
|
||||
@@ -92,21 +110,7 @@ async def async_get_triggers(
|
||||
hass: HomeAssistant, device_id: str
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Return a list of triggers for BTHome BLE devices."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get(device_id)
|
||||
assert device is not None
|
||||
config_entries = [
|
||||
hass.config_entries.async_get_entry(entry_id)
|
||||
for entry_id in device.config_entries
|
||||
]
|
||||
bthome_config_entry = next(
|
||||
iter(entry for entry in config_entries if entry and entry.domain == DOMAIN),
|
||||
None,
|
||||
)
|
||||
assert bthome_config_entry is not None
|
||||
event_classes: list[str] = bthome_config_entry.data.get(
|
||||
CONF_DISCOVERED_EVENT_CLASSES, []
|
||||
)
|
||||
event_classes = get_event_classes_by_device_id(hass, device_id)
|
||||
return [
|
||||
{
|
||||
# Required fields of TRIGGER_BASE_SCHEMA
|
||||
@@ -118,14 +122,7 @@ async def async_get_triggers(
|
||||
CONF_SUBTYPE: event_type,
|
||||
}
|
||||
for event_class in event_classes
|
||||
for event_type in TRIGGERS_BY_EVENT_CLASS.get(
|
||||
event_class.split("_")[0],
|
||||
# If the device has multiple buttons they will have
|
||||
# event classes like button_1 button_2, button_3, etc
|
||||
# but if there is only one button then it will be
|
||||
# button without a number postfix.
|
||||
(),
|
||||
)
|
||||
for event_type in get_event_types_by_event_class(event_class)
|
||||
]
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user