mirror of
https://github.com/home-assistant/core.git
synced 2026-06-11 19:51:39 +02:00
Compare commits
202 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a1baa9573 | |||
| 8fed48d8ac | |||
| f5f80e7080 | |||
| 1e18b77c67 | |||
| 0dec701acd | |||
| 851facd826 | |||
| e56c221eb1 | |||
| a4eba86a6c | |||
| a25a55737f | |||
| 1b582f4089 | |||
| c83323894c | |||
| ac5e1f178b | |||
| 00a48df8cb | |||
| 0d67cc0795 | |||
| a2477d71fb | |||
| c0b5dec23b | |||
| 64a68f38f0 | |||
| d8ce17aaa3 | |||
| da035f1ca3 | |||
| 5a27b29003 | |||
| e04600eaec | |||
| 0b45db67e0 | |||
| 3380a8ff29 | |||
| fd21674ca1 | |||
| 2e4185840a | |||
| 1126e89d32 | |||
| 08f4774e64 | |||
| c02147f386 | |||
| f48a4720e5 | |||
| 5b083f7959 | |||
| 7bedf8074d | |||
| d656a1c091 | |||
| 06d8570e2c | |||
| 392f7b7260 | |||
| eb4568fe54 | |||
| 2ab3e0770f | |||
| 3435cfeaab | |||
| 34956c1548 | |||
| 67740405a8 | |||
| 866437a0fd | |||
| 03523f96c2 | |||
| bbf91d7ee4 | |||
| 130ca851f6 | |||
| f5b8e8ba81 | |||
| d8182508bb | |||
| 5a00de9e87 | |||
| b1f2e80f40 | |||
| 1684ea7870 | |||
| 742a6282f7 | |||
| ae23d0e3e7 | |||
| 158a8b8c69 | |||
| 102cb4b69e | |||
| f3a3f4cde4 | |||
| 19880b4214 | |||
| 27599cbfe3 | |||
| 54cb4a7946 | |||
| 4a5ee9e4ee | |||
| 8111667c1f | |||
| 03eb139f6e | |||
| 426213dd29 | |||
| 8e659530da | |||
| 76c28444e8 | |||
| f4a9514d35 | |||
| 6f5f59608c | |||
| fc8040784e | |||
| 8e77ef3e1b | |||
| 95257e36ef | |||
| 89a600dc34 | |||
| 2b530a1dfa | |||
| 9a01a0d758 | |||
| 145538c563 | |||
| 6f09fc074d | |||
| 372c6697a0 | |||
| bbd82ce511 | |||
| bcb0908d0a | |||
| da5f497091 | |||
| c28e215ae6 | |||
| ee2fb6e150 | |||
| 9bdb2e21fe | |||
| d2277ddbd7 | |||
| cbce4b8e76 | |||
| bb5d3fe67f | |||
| 33753460ab | |||
| d815487f7f | |||
| 5d3f7001ab | |||
| 2287c92f88 | |||
| b1610163f1 | |||
| 1cfae60c03 | |||
| 49d1e0ea5b | |||
| 05f4e28c86 | |||
| 2b5fc802c4 | |||
| 9b5038d741 | |||
| 7c33b953d3 | |||
| 5ffd772868 | |||
| 392c7f97c8 | |||
| 0672c940a4 | |||
| 03b0b4ad8b | |||
| 9b84fc9dba | |||
| e38e6ecec8 | |||
| 37e4f1ab32 | |||
| 18d17a5346 | |||
| d10ede2264 | |||
| 05088bf991 | |||
| fbf14c63c0 | |||
| 397c28b9b6 | |||
| 676a8c39eb | |||
| d4accebb3b | |||
| b8bdd2c47c | |||
| 828ec639dd | |||
| df4fbc91f9 | |||
| 1a4a95df83 | |||
| e4b5818b56 | |||
| c9ad482293 | |||
| 78cc155e56 | |||
| 145639d048 | |||
| 3e3e9af30d | |||
| c27e43c570 | |||
| 4f4aeff2b4 | |||
| 850cc27824 | |||
| e19c063ef1 | |||
| 707742f720 | |||
| f58e0e5234 | |||
| c3d6ad029f | |||
| 630f442042 | |||
| 62419789b9 | |||
| f2f5a55165 | |||
| c6a57bc81a | |||
| 4171f566e9 | |||
| 0ac9834d93 | |||
| d7673a08c8 | |||
| 35cb7c6147 | |||
| d098622021 | |||
| f88e757e51 | |||
| 653e6a43fa | |||
| 1462e7a181 | |||
| e34d821f7d | |||
| 02b4442a6c | |||
| 809571443c | |||
| d59398e0ea | |||
| 9c9695d0ba | |||
| 3fbdbb12e2 | |||
| a29f2907f7 | |||
| 83534f286e | |||
| 4fe93f9c64 | |||
| fd8789d599 | |||
| d0b34dfe92 | |||
| 390766ba3a | |||
| 3a46d1088b | |||
| 26d56b8218 | |||
| 6ee819cdc3 | |||
| 1cf8fe4d0b | |||
| c5f93cdd72 | |||
| 42136f1464 | |||
| 34f3452280 | |||
| ba9248cc94 | |||
| 018cd1333e | |||
| c72d723e0d | |||
| b9b36d9e12 | |||
| b6f38c3cbb | |||
| a0162d2ff0 | |||
| b6f018873b | |||
| 43e21322ea | |||
| 86ccc59a5f | |||
| 2fce2547c7 | |||
| 6b40278d08 | |||
| 05bb8b94fa | |||
| 5ac3a8cdde | |||
| 266fccf0cf | |||
| a1e6a6f9a2 | |||
| 2fe406c6ff | |||
| e1249fef8f | |||
| 6f61e97f8e | |||
| b65751e8ac | |||
| ef4bf77b24 | |||
| 977a9ecdd2 | |||
| 9e79eba970 | |||
| 40073e598c | |||
| 627d5cc110 | |||
| b1dbeca9ed | |||
| 059bc8d676 | |||
| 085f794407 | |||
| 3996db289d | |||
| 291585e48e | |||
| d9a125ce9b | |||
| 786c957909 | |||
| dd6830f1c5 | |||
| 4dbe58afc6 | |||
| 6c72d4337d | |||
| fcff5229d9 | |||
| 8edd813d4b | |||
| 509866c0eb | |||
| 9db5860d6b | |||
| 6917223cb3 | |||
| cc4637a703 | |||
| 2b0d14d71e | |||
| d0d85d8844 | |||
| eea3d9d4c4 | |||
| 48a690b267 | |||
| 07dc2346de | |||
| 711830b01f | |||
| f9fea56a8c | |||
| 8aac0c5b6e |
@@ -38,7 +38,7 @@ jobs:
|
||||
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -292,7 +292,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -471,7 +471,7 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -518,7 +518,7 @@ jobs:
|
||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
|
||||
+11
-11
@@ -31,12 +31,12 @@
|
||||
# - GITHUB_TOKEN
|
||||
#
|
||||
# Custom actions used:
|
||||
# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
# - github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
# - github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
|
||||
#
|
||||
# Container images used:
|
||||
# - ghcr.io/github/gh-aw-firewall/agent:0.25.46
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
- name: Checkout .github and .agents folders
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
@@ -352,7 +352,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -372,7 +372,7 @@ jobs:
|
||||
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Create gh-aw temp directory
|
||||
@@ -961,7 +961,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1100,7 +1100,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1127,7 +1127,7 @@ jobs:
|
||||
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository for patch context
|
||||
if: needs.agent.outputs.has_patch == 'true'
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
# --- Threat Detection ---
|
||||
@@ -1325,7 +1325,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1383,7 +1383,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@b11be78086764c43fa463398aed7ffdcf40549c1 # v0.77.0
|
||||
uses: github/gh-aw-actions/setup@73ed520ae4ecd087a485e1991605595978b32ac1 # v0.78.1
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
|
||||
+24
-24
@@ -98,7 +98,7 @@ jobs:
|
||||
skip_coverage: ${{ steps.info.outputs.skip_coverage }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Generate partial Python venv restore key
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Register problem matchers
|
||||
@@ -291,7 +291,7 @@ jobs:
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
@@ -318,7 +318,7 @@ jobs:
|
||||
- script/hassfest/docker/Dockerfile
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Register hadolint problem matcher
|
||||
@@ -341,7 +341,7 @@ jobs:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -404,7 +404,7 @@ jobs:
|
||||
echo "version=$(grep '^uv==' requirements.txt | cut -d'=' -f3)" >> "$GITHUB_OUTPUT"
|
||||
- name: Set up uv
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
with:
|
||||
version: ${{ steps.read-uv-version.outputs.version }}
|
||||
- name: Create Python virtual environment
|
||||
@@ -469,7 +469,7 @@ jobs:
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install additional OS dependencies
|
||||
@@ -512,7 +512,7 @@ jobs:
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
@@ -548,7 +548,7 @@ jobs:
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
@@ -576,7 +576,7 @@ jobs:
|
||||
&& github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Dependency review
|
||||
@@ -603,7 +603,7 @@ jobs:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -654,7 +654,7 @@ jobs:
|
||||
|| github.event.inputs.pylint-only == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
@@ -707,7 +707,7 @@ jobs:
|
||||
&& (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
@@ -758,7 +758,7 @@ jobs:
|
||||
|| github.event.inputs.mypy-only == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
@@ -825,7 +825,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install additional OS dependencies
|
||||
@@ -889,7 +889,7 @@ jobs:
|
||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install additional OS dependencies
|
||||
@@ -1030,7 +1030,7 @@ jobs:
|
||||
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install additional OS dependencies
|
||||
@@ -1179,7 +1179,7 @@ jobs:
|
||||
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install additional OS dependencies
|
||||
@@ -1317,7 +1317,7 @@ jobs:
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download all coverage artifacts
|
||||
@@ -1326,7 +1326,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
@@ -1355,7 +1355,7 @@ jobs:
|
||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install additional OS dependencies
|
||||
@@ -1476,7 +1476,7 @@ jobs:
|
||||
- pytest-partial
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download all coverage artifacts
|
||||
@@ -1485,7 +1485,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
||||
@@ -1513,7 +1513,7 @@ jobs:
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
report_type: test_results
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -23,16 +23,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
os: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.14
|
||||
rev: v0.15.15
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
|
||||
@@ -96,6 +96,7 @@ homeassistant.components.aprs.*
|
||||
homeassistant.components.apsystems.*
|
||||
homeassistant.components.aqualogic.*
|
||||
homeassistant.components.aquostv.*
|
||||
homeassistant.components.aqvify.*
|
||||
homeassistant.components.aranet.*
|
||||
homeassistant.components.arcam_fmj.*
|
||||
homeassistant.components.arris_tg2492lg.*
|
||||
|
||||
Generated
+8
-2
@@ -162,6 +162,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
|
||||
/homeassistant/components/aquacell/ @Jordi1990
|
||||
/tests/components/aquacell/ @Jordi1990
|
||||
/homeassistant/components/aqvify/ @astrandb
|
||||
/tests/components/aqvify/ @astrandb
|
||||
/homeassistant/components/aranet/ @aschmitz @thecode @anrijs
|
||||
/tests/components/aranet/ @aschmitz @thecode @anrijs
|
||||
/homeassistant/components/arcam_fmj/ @elupus
|
||||
@@ -574,8 +576,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/flo/ @dmulcahey
|
||||
/homeassistant/components/flume/ @ChrisMandich @bdraco @jeeftor
|
||||
/tests/components/flume/ @ChrisMandich @bdraco @jeeftor
|
||||
/homeassistant/components/fluss/ @fluss
|
||||
/tests/components/fluss/ @fluss
|
||||
/homeassistant/components/fluss/ @fluss @Marcello17
|
||||
/tests/components/fluss/ @fluss @Marcello17
|
||||
/homeassistant/components/flux_led/ @icemanch
|
||||
/tests/components/flux_led/ @icemanch
|
||||
/homeassistant/components/forecast_solar/ @klaasnicolaas @frenck
|
||||
@@ -945,6 +947,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/kiosker/ @Claeysson
|
||||
/homeassistant/components/kitchen_sink/ @home-assistant/core
|
||||
/tests/components/kitchen_sink/ @home-assistant/core
|
||||
/homeassistant/components/klik_aan_klik_uit/ @Phunkafizer
|
||||
/tests/components/klik_aan_klik_uit/ @Phunkafizer
|
||||
/homeassistant/components/kmtronic/ @dgomes
|
||||
/tests/components/kmtronic/ @dgomes
|
||||
/homeassistant/components/knocki/ @joostlek @jgatto1 @JakeBosh
|
||||
@@ -1082,6 +1086,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/mediaroom/ @dgomes
|
||||
/homeassistant/components/melcloud/ @erwindouna
|
||||
/tests/components/melcloud/ @erwindouna
|
||||
/homeassistant/components/melcloud_home/ @erwindouna
|
||||
/tests/components/melcloud_home/ @erwindouna
|
||||
/homeassistant/components/melissa/ @kennedyshead
|
||||
/tests/components/melissa/ @kennedyshead
|
||||
/homeassistant/components/melnor/ @vanstinator
|
||||
|
||||
@@ -116,10 +116,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||
"""Migrate old config entry."""
|
||||
|
||||
# This means the user has downgraded from a future version
|
||||
if entry.version > 2:
|
||||
return False
|
||||
|
||||
# 1.1 Migrate config_entry to add advanced ssl settings
|
||||
if entry.version == 1 and entry.minor_version == 1:
|
||||
new_minor_version = 2
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"""The AirVisual Pro integration."""
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
DeviceInfo,
|
||||
format_mac,
|
||||
)
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -25,6 +29,12 @@ class AirVisualProEntity(CoordinatorEntity[AirVisualProCoordinator]):
|
||||
"""Return device registry information for this entity."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.coordinator.data["serial_number"])},
|
||||
connections={
|
||||
(
|
||||
CONNECTION_NETWORK_MAC,
|
||||
format_mac(self.coordinator.data["status"]["mac_address"]),
|
||||
)
|
||||
},
|
||||
manufacturer="AirVisual",
|
||||
model=self.coordinator.data["status"]["model"],
|
||||
name=self.coordinator.data["settings"]["node_name"],
|
||||
|
||||
@@ -65,10 +65,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
|
||||
if entry.version > 1:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if entry.version == 1 and entry.minor_version < 3:
|
||||
if CONF_SITE in entry.data:
|
||||
# Site in data (wrong place), just move to login data
|
||||
|
||||
@@ -6,7 +6,7 @@ from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator, alexa_api_call
|
||||
from .entity import AmazonServiceEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
@@ -49,4 +49,5 @@ class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity):
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle button press action."""
|
||||
await self.coordinator.api.call_routine(self._routine)
|
||||
async with alexa_api_call(self.coordinator):
|
||||
await self.coordinator.api.call_routine(self._routine)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Support for Alexa Devices."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import timedelta
|
||||
|
||||
from aioamazondevices.api import AmazonEchoApi
|
||||
@@ -19,7 +21,11 @@ from aiohttp import ClientSession
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@@ -29,6 +35,65 @@ from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
SCAN_INTERVAL = 300
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def alexa_api_call(
|
||||
coordinator: DataUpdateCoordinator | None = None,
|
||||
) -> AsyncGenerator[None]:
|
||||
"""Handle common Alexa API exceptions as HomeAssistantError."""
|
||||
try:
|
||||
yield
|
||||
except CannotAuthenticate as err:
|
||||
if coordinator:
|
||||
coordinator.last_update_success = False
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except CannotConnect as err:
|
||||
if coordinator:
|
||||
coordinator.last_update_success = False
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect_with_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except (CannotRetrieveData, ValueError) as err:
|
||||
if coordinator:
|
||||
coordinator.last_update_success = False
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_retrieve_data_with_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def alexa_config_entry_errors() -> AsyncGenerator[None]:
|
||||
"""Handle common Alexa API exceptions as ConfigEntry errors."""
|
||||
try:
|
||||
yield
|
||||
except CannotAuthenticate as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except (CannotConnect, TimeoutError) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect_with_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except (CannotRetrieveData, ValueError, KeyError, StopIteration) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_retrieve_data_with_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
|
||||
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
|
||||
|
||||
|
||||
@@ -113,6 +178,12 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except ValueError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_retrieve_data_with_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
else:
|
||||
current_devices = set(data.keys())
|
||||
if stale_devices := self.previous_devices - current_devices:
|
||||
@@ -169,26 +240,8 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
|
||||
async def sync_history_state(self) -> None:
|
||||
"""Sync history state."""
|
||||
try:
|
||||
async with alexa_config_entry_errors():
|
||||
self._vocal_records = await self.api.sync_history_state()
|
||||
except CannotAuthenticate as e:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(e)},
|
||||
) from e
|
||||
except CannotConnect as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect_with_error",
|
||||
translation_placeholders={"error": repr(e)},
|
||||
) from e
|
||||
except BaseException as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_retrieve_data_with_error",
|
||||
translation_placeholders={"error": repr(e)},
|
||||
) from e
|
||||
|
||||
async def history_state_event_handler(
|
||||
self, vocal_records: dict[str, AmazonVocalRecord]
|
||||
@@ -204,26 +257,8 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
|
||||
async def sync_media_state(self) -> None:
|
||||
"""Sync media state."""
|
||||
try:
|
||||
async with alexa_config_entry_errors():
|
||||
await self.api.sync_media_state()
|
||||
except CannotAuthenticate as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except (CannotConnect, TimeoutError) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect_with_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except (CannotRetrieveData, ValueError) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_retrieve_data_with_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
async def media_state_event_handler(
|
||||
self, media_state: dict[str, AmazonMediaState]
|
||||
|
||||
@@ -12,7 +12,18 @@ from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"}
|
||||
TO_REDACT = {
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
"access_token",
|
||||
"adp_token",
|
||||
"device_private_key",
|
||||
"refresh_token",
|
||||
"store_authentication_cookie",
|
||||
"title",
|
||||
"website_cookies",
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==14.0.0"]
|
||||
"requirements": ["aioamazondevices==14.0.3"]
|
||||
}
|
||||
|
||||
@@ -22,9 +22,8 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import _LOGGER
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator, alexa_api_call
|
||||
from .entity import AmazonEntity
|
||||
from .utils import alexa_api_call
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -216,16 +215,15 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
|
||||
provider = media_type.value if isinstance(media_type, MediaType) else media_type
|
||||
await self.async_call_alexa_music(media_id, provider)
|
||||
|
||||
@alexa_api_call
|
||||
async def async_call_alexa_music(
|
||||
self, search_phrase: str, provider_id: str
|
||||
) -> None:
|
||||
"""Call alexa music."""
|
||||
await self.coordinator.api.call_alexa_music(
|
||||
self.device, search_phrase, provider_id
|
||||
)
|
||||
async with alexa_api_call(self.coordinator):
|
||||
await self.coordinator.api.call_alexa_music(
|
||||
self.device, search_phrase, provider_id
|
||||
)
|
||||
|
||||
@alexa_api_call
|
||||
async def async_set_device_volume(self, volume: int) -> None:
|
||||
"""Set the device volume."""
|
||||
_LOGGER.debug(
|
||||
@@ -233,7 +231,8 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
|
||||
self.device.serial_number,
|
||||
volume,
|
||||
)
|
||||
await self.coordinator.api.set_device_volume(self.device, volume)
|
||||
async with alexa_api_call(self.coordinator):
|
||||
await self.coordinator.api.set_device_volume(self.device, volume)
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set the volume level (0.0 to 1.0)."""
|
||||
@@ -263,12 +262,12 @@ class AlexaDevicesMediaPlayer(AmazonEntity, MediaPlayerEntity):
|
||||
await self.async_set_volume_level(target_volume / 100)
|
||||
self._prev_volume = None
|
||||
|
||||
@alexa_api_call
|
||||
async def _send_media_command(self, command: AmazonMediaControls) -> None:
|
||||
_LOGGER.debug(
|
||||
"Sending media command '%s' to %s", command, self.device.serial_number
|
||||
)
|
||||
await self.coordinator.api.send_media_command(self.device, command)
|
||||
async with alexa_api_call(self.coordinator):
|
||||
await self.coordinator.api.send_media_command(self.device, command)
|
||||
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Send stop command."""
|
||||
|
||||
@@ -12,9 +12,8 @@ from homeassistant.components.notify import NotifyEntity, NotifyEntityDescriptio
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .coordinator import AmazonConfigEntry, alexa_api_call
|
||||
from .entity import AmazonEntity
|
||||
from .utils import alexa_api_call
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -80,10 +79,11 @@ class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
|
||||
|
||||
entity_description: AmazonNotifyEntityDescription
|
||||
|
||||
@alexa_api_call
|
||||
async def async_send_message(
|
||||
self, message: str, title: str | None = None, **kwargs: Any
|
||||
) -> None:
|
||||
"""Send a message."""
|
||||
|
||||
await self.entity_description.method(self.coordinator.api, self.device, message)
|
||||
async with alexa_api_call(self.coordinator):
|
||||
await self.entity_description.method(
|
||||
self.coordinator.api, self.device, message
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
|
||||
from .const import DOMAIN, INFO_SKILLS_MAPPING
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .coordinator import AmazonConfigEntry, alexa_api_call
|
||||
|
||||
ATTR_TEXT_COMMAND = "text_command"
|
||||
ATTR_SOUND = "sound"
|
||||
@@ -85,13 +85,15 @@ async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
|
||||
translation_key="invalid_sound_value",
|
||||
translation_placeholders={"sound": value},
|
||||
)
|
||||
await coordinator.api.call_alexa_sound(
|
||||
coordinator.data[device.serial_number], value
|
||||
)
|
||||
async with alexa_api_call():
|
||||
await coordinator.api.call_alexa_sound(
|
||||
coordinator.data[device.serial_number], value
|
||||
)
|
||||
elif attribute == ATTR_TEXT_COMMAND:
|
||||
await coordinator.api.call_alexa_text_command(
|
||||
coordinator.data[device.serial_number], value
|
||||
)
|
||||
async with alexa_api_call():
|
||||
await coordinator.api.call_alexa_text_command(
|
||||
coordinator.data[device.serial_number], value
|
||||
)
|
||||
elif attribute == ATTR_INFO_SKILL:
|
||||
info_skill = INFO_SKILLS_MAPPING.get(value)
|
||||
if info_skill not in ALEXA_INFO_SKILLS:
|
||||
@@ -100,9 +102,10 @@ async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
|
||||
translation_key="invalid_info_skill_value",
|
||||
translation_placeholders={"info_skill": value},
|
||||
)
|
||||
await coordinator.api.call_alexa_info_skill(
|
||||
coordinator.data[device.serial_number], info_skill
|
||||
)
|
||||
async with alexa_api_call():
|
||||
await coordinator.api.call_alexa_info_skill(
|
||||
coordinator.data[device.serial_number], info_skill
|
||||
)
|
||||
|
||||
|
||||
async def async_send_sound_notification(call: ServiceCall) -> None:
|
||||
|
||||
@@ -14,13 +14,9 @@ from homeassistant.components.switch import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .coordinator import AmazonConfigEntry, alexa_api_call
|
||||
from .entity import AmazonEntity
|
||||
from .utils import (
|
||||
alexa_api_call,
|
||||
async_remove_dnd_from_virtual_group,
|
||||
async_update_unique_id,
|
||||
)
|
||||
from .utils import async_remove_dnd_from_virtual_group, async_update_unique_id
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -90,7 +86,6 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
||||
|
||||
entity_description: AmazonSwitchEntityDescription
|
||||
|
||||
@alexa_api_call
|
||||
async def _switch_set_state(self, state: bool) -> None:
|
||||
"""Set desired switch state."""
|
||||
method = getattr(self.coordinator.api, self.entity_description.method)
|
||||
@@ -98,7 +93,8 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
||||
if TYPE_CHECKING:
|
||||
assert method is not None
|
||||
|
||||
await method(self.device, state)
|
||||
async with alexa_api_call(self.coordinator):
|
||||
await method(self.device, state)
|
||||
self.coordinator.data[self.device.serial_number].sensors[
|
||||
self.entity_description.key
|
||||
].value = state
|
||||
|
||||
@@ -1,54 +1,19 @@
|
||||
"""Utils for Alexa Devices."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
|
||||
from aioamazondevices.const.schedules import (
|
||||
NOTIFICATION_ALARM,
|
||||
NOTIFICATION_REMINDER,
|
||||
NOTIFICATION_TIMER,
|
||||
)
|
||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .coordinator import AmazonDevicesCoordinator
|
||||
from .entity import AmazonEntity
|
||||
|
||||
|
||||
def alexa_api_call[_T: AmazonEntity, **_P](
|
||||
func: Callable[Concatenate[_T, _P], Awaitable[None]],
|
||||
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
|
||||
"""Catch Alexa API call exceptions."""
|
||||
|
||||
@wraps(func)
|
||||
async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
"""Wrap all command methods."""
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
except CannotConnect as err:
|
||||
self.coordinator.last_update_success = False
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect_with_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except CannotRetrieveData as err:
|
||||
self.coordinator.last_update_success = False
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_retrieve_data_with_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
return cmd_wrapper
|
||||
|
||||
|
||||
async def async_update_unique_id(
|
||||
|
||||
@@ -75,10 +75,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnovaConfigEntry) -> b
|
||||
"""Migrate entry."""
|
||||
_LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
|
||||
|
||||
if entry.version > 1:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if entry.version == 1 and entry.minor_version == 1:
|
||||
new_data = {**entry.data}
|
||||
if CONF_DEVICES in new_data:
|
||||
|
||||
@@ -178,10 +178,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
|
||||
"""Migrate entry."""
|
||||
LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
|
||||
|
||||
if entry.version > 2:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 1:
|
||||
# Correct broken device migration in Home Assistant Core 2025.7.0b0-2025.7.0b1
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Coordinator for the Anthropic integration."""
|
||||
|
||||
import datetime
|
||||
import re
|
||||
|
||||
import anthropic
|
||||
|
||||
@@ -20,15 +19,12 @@ UPDATE_INTERVAL_DISCONNECTED = datetime.timedelta(minutes=1)
|
||||
type AnthropicConfigEntry = ConfigEntry[AnthropicCoordinator]
|
||||
|
||||
|
||||
_model_short_form = re.compile(r"[^\d]-\d$")
|
||||
|
||||
|
||||
@callback
|
||||
def model_alias(model_id: str) -> str:
|
||||
"""Resolve alias from versioned model name."""
|
||||
if model_id[-2:-1] != "-" and not model_id.endswith("-preview"):
|
||||
model_id = model_id[:-9]
|
||||
if _model_short_form.search(model_id):
|
||||
if model_id.endswith("-4"):
|
||||
return model_id + "-0"
|
||||
return model_id
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["anthropic==0.96.0"]
|
||||
"requirements": ["anthropic==0.108.0"]
|
||||
}
|
||||
|
||||
@@ -65,18 +65,18 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
|
||||
]
|
||||
self._model_list_cache[entry.entry_id] = model_list
|
||||
|
||||
if "opus" in model:
|
||||
family = "claude-opus"
|
||||
elif "sonnet" in model:
|
||||
family = "claude-sonnet"
|
||||
else:
|
||||
family = "claude-haiku"
|
||||
family = (
|
||||
model.removeprefix("claude-")
|
||||
.removesuffix("-preview")
|
||||
.translate(str.maketrans("", "", "0123456789-."))
|
||||
or "haiku"
|
||||
)
|
||||
|
||||
suggested_model = next(
|
||||
(
|
||||
model_option["value"]
|
||||
for model_option in sorted(
|
||||
(m for m in model_list if family in m["value"]),
|
||||
(m for m in model_list if f"claude-{family}" in m["value"]),
|
||||
key=lambda x: x["value"],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
@@ -59,7 +59,6 @@ ATTR_EXTERNAL_URL = "external_url"
|
||||
ATTR_INTERNAL_URL = "internal_url"
|
||||
ATTR_LOCATION_NAME = "location_name"
|
||||
ATTR_INSTALLATION_TYPE = "installation_type"
|
||||
ATTR_REQUIRES_API_PASSWORD = "requires_api_password"
|
||||
ATTR_UUID = "uuid"
|
||||
ATTR_VERSION = "version"
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""The Aqvify integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AqvifyConfigEntry, AqvifyCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AqvifyConfigEntry) -> bool:
|
||||
"""Set up Aqvify from a config entry."""
|
||||
|
||||
coordinator = AqvifyCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AqvifyConfigEntry) -> bool:
|
||||
"""Unload Aqvify config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Config flow for the Aqvify integration."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from pyaqvify import AqvifyAPI, AqvifyAuthException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AqvifyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Aqvify."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
hub = AqvifyAPI(
|
||||
user_input[CONF_API_KEY],
|
||||
websession=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
account_data = await hub.async_get_account_id()
|
||||
except AqvifyAuthException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except ClientResponseError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(account_data.account_id)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(), data_updates=user_input
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title="Aqvify", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"aqvify_url": "https://app.aqvify.com/User",
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle re-authentication confirmation."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
api_client = AqvifyAPI(
|
||||
user_input[CONF_API_KEY],
|
||||
websession=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
account_data = await api_client.async_get_account_id()
|
||||
except AqvifyAuthException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except ClientResponseError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(account_data.account_id)
|
||||
self._abort_if_unique_id_mismatch()
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), data_updates=user_input
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: Mapping[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""User initiated reconfiguration."""
|
||||
return await self.async_step_user()
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Constants for the Aqvify integration."""
|
||||
|
||||
DOMAIN = "aqvify"
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Coordinator for Aqvify integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from pyaqvify import AqvifyAPI, AqvifyAuthException, AqvifyDeviceData, AqvifyDevices
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
type AqvifyConfigEntry = ConfigEntry[AqvifyCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AqvifyCoordinatorData:
|
||||
"""Data class for storing coordinator data."""
|
||||
|
||||
devices: AqvifyDevices
|
||||
device_data: dict[str, AqvifyDeviceData]
|
||||
|
||||
|
||||
class AqvifyCoordinator(DataUpdateCoordinator[AqvifyCoordinatorData]):
|
||||
"""Data update coordinator for Aqvify devices."""
|
||||
|
||||
config_entry: AqvifyConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: AqvifyConfigEntry) -> None:
|
||||
"""Initialize the Aqvify data update coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
config_entry=entry,
|
||||
)
|
||||
|
||||
self.api_client = AqvifyAPI(
|
||||
entry.data[CONF_API_KEY], websession=async_get_clientsession(hass)
|
||||
)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
try:
|
||||
await self.api_client.async_get_account_id()
|
||||
except AqvifyAuthException:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_api_key",
|
||||
) from None
|
||||
except ClientResponseError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={
|
||||
"entry": self.config_entry.title,
|
||||
},
|
||||
) from err
|
||||
except TimeoutError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_timeout",
|
||||
translation_placeholders={
|
||||
"entry": self.config_entry.title,
|
||||
},
|
||||
) from err
|
||||
|
||||
async def _async_update_data(self) -> AqvifyCoordinatorData:
|
||||
"""Fetch device state."""
|
||||
try:
|
||||
devices = await self.api_client.async_get_devices()
|
||||
except AqvifyAuthException:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_api_key",
|
||||
) from None
|
||||
except ClientResponseError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={
|
||||
"entry": self.config_entry.title,
|
||||
},
|
||||
) from err
|
||||
except TimeoutError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_timeout",
|
||||
translation_placeholders={
|
||||
"entry": self.config_entry.title,
|
||||
},
|
||||
) from err
|
||||
|
||||
device_data = {}
|
||||
for device in devices.devices.values():
|
||||
try:
|
||||
device_key = str(device.device_key)
|
||||
device_data[
|
||||
device_key
|
||||
] = await self.api_client.async_get_device_latest_data(device_key)
|
||||
except AqvifyAuthException:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_api_key",
|
||||
) from None
|
||||
except ClientResponseError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={
|
||||
"entry": self.config_entry.title,
|
||||
},
|
||||
) from err
|
||||
except TimeoutError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_timeout",
|
||||
translation_placeholders={
|
||||
"entry": self.config_entry.title,
|
||||
},
|
||||
) from err
|
||||
|
||||
return AqvifyCoordinatorData(
|
||||
devices=devices,
|
||||
device_data=device_data,
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Diagnostics platform for Aqvify integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AqvifyConfigEntry
|
||||
|
||||
TO_REDACT = [CONF_API_KEY]
|
||||
TO_REDACT_AQVIFY = ["name"]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AqvifyConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
device_list_raw_data = entry.runtime_data.data.devices.raw
|
||||
device_data_raw_data = {
|
||||
key: device.raw_data
|
||||
for key, device in entry.runtime_data.data.device_data.items()
|
||||
}
|
||||
|
||||
return {
|
||||
"entry_data": async_redact_data(entry.data, TO_REDACT),
|
||||
"devices": async_redact_data(device_list_raw_data, TO_REDACT_AQVIFY),
|
||||
"device_data": device_data_raw_data,
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Defines a base Aqvify entity."""
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AqvifyCoordinator
|
||||
|
||||
|
||||
class AqvifyBaseEntity(CoordinatorEntity[AqvifyCoordinator]):
|
||||
"""Defines a base Aqvify entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AqvifyCoordinator,
|
||||
description: EntityDescription,
|
||||
device_key: str,
|
||||
) -> None:
|
||||
"""Initialize the Aqvify entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
account_id = self.coordinator.config_entry.unique_id
|
||||
self.device_key = device_key
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{account_id}_{device_key}")},
|
||||
name=coordinator.data.devices.devices[device_key].name,
|
||||
manufacturer="Aqvify",
|
||||
configuration_url="https://app.aqvify.com",
|
||||
serial_number=device_key,
|
||||
)
|
||||
self._attr_unique_id = f"{account_id}_{device_key}_{description.key}"
|
||||
self.entity_description = description
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"meter_value": {
|
||||
"default": "mdi:waves-arrow-up"
|
||||
},
|
||||
"water_level": {
|
||||
"default": "mdi:waves"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "aqvify",
|
||||
"name": "Aqvify",
|
||||
"codeowners": ["@astrandb"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aqvify",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyaqvify"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyaqvify==0.0.9"]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
No actions in this integration.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Entities of this integration do not explicitly subscribe to events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: todo
|
||||
integration-owner: todo
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: todo
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Sensor platform for Aqvify integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from pyaqvify import AqvifyDeviceData
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
StateType,
|
||||
)
|
||||
from homeassistant.const import UnitOfLength
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AqvifyConfigEntry
|
||||
from .entity import AqvifyBaseEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates.
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AqvifySensorEntityDescription(SensorEntityDescription):
|
||||
"""Description of an Aqvify sensor entity."""
|
||||
|
||||
value_fn: Callable[[AqvifyDeviceData], float | int | None]
|
||||
|
||||
|
||||
ENTITIES: tuple[AqvifySensorEntityDescription, ...] = (
|
||||
AqvifySensorEntityDescription(
|
||||
key="meter_value",
|
||||
translation_key="meter_value",
|
||||
native_unit_of_measurement=UnitOfLength.METERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda value: value.meter_value,
|
||||
),
|
||||
AqvifySensorEntityDescription(
|
||||
key="water_level",
|
||||
translation_key="water_level",
|
||||
native_unit_of_measurement=UnitOfLength.METERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda value: value.water_level,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AqvifyConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Aqvify sensor entities from a config entry."""
|
||||
async_add_entities(
|
||||
AqvifySensor(entry.runtime_data, description, device_key)
|
||||
for description in ENTITIES
|
||||
for device_key in entry.runtime_data.data.devices.devices
|
||||
)
|
||||
|
||||
|
||||
class AqvifySensor(AqvifyBaseEntity, SensorEntity):
|
||||
"""Representation of an Aqvify sensor entity."""
|
||||
|
||||
entity_description: AqvifySensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.data.device_data[self.device_key]
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"unique_id_mismatch": "The entered API key corresponds to a different account."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::aqvify::config::step::user::data_description::api_key%]"
|
||||
},
|
||||
"description": "Reauthentication required. Please enter your updated API key."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "Your Aqvify API key"
|
||||
},
|
||||
"description": "Navigate to your [Aqvify account]({aqvify_url}), copy your API key, and paste it below."
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"meter_value": {
|
||||
"name": "Meter value"
|
||||
},
|
||||
"water_level": {
|
||||
"name": "Water level"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"api_error": {
|
||||
"message": "An error occurred while communicating with the Aqvify API for {entry}"
|
||||
},
|
||||
"api_timeout": {
|
||||
"message": "Timeout occurred while communicating with the Aqvify API for {entry}"
|
||||
},
|
||||
"invalid_api_key": {
|
||||
"message": "Invalid API key. Please verify your API key and try to reauthenticate."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1816,6 +1816,11 @@ class PipelineInput:
|
||||
await self.run.text_to_speech(tts_input)
|
||||
|
||||
except PipelineError as err:
|
||||
if self.run.tts_stream:
|
||||
# Clean up TTS stream
|
||||
self.run.tts_stream.delete()
|
||||
self.run.tts_stream = None
|
||||
|
||||
self.run.process_event(
|
||||
PipelineEvent(
|
||||
PipelineEventType.ERROR,
|
||||
@@ -1885,15 +1890,17 @@ class PipelineInput:
|
||||
):
|
||||
prepare_tasks.append(self.run.prepare_recognize_intent(self.session))
|
||||
|
||||
if prepare_tasks:
|
||||
await asyncio.gather(*prepare_tasks)
|
||||
|
||||
# Do TTS prepare separately so we don't create a ResultStream if the
|
||||
# pipeline is invalid.
|
||||
if (
|
||||
start_stage_index
|
||||
<= PIPELINE_STAGE_ORDER.index(PipelineStage.TTS)
|
||||
<= end_stage_index
|
||||
):
|
||||
prepare_tasks.append(self.run.prepare_text_to_speech())
|
||||
|
||||
if prepare_tasks:
|
||||
await asyncio.gather(*prepare_tasks)
|
||||
await self.run.prepare_text_to_speech()
|
||||
|
||||
|
||||
class PipelinePreferred(CollectionError):
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
from dataclasses import asdict
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from hassil.parse_expression import parse_sentence
|
||||
from hassil.parser import ParseError
|
||||
from hassil.util import (
|
||||
PUNCTUATION_END,
|
||||
PUNCTUATION_END_WORD,
|
||||
@@ -164,6 +167,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
[cv.string],
|
||||
has_one_non_empty_item,
|
||||
has_no_punctuation,
|
||||
is_valid_sentence,
|
||||
),
|
||||
}
|
||||
],
|
||||
@@ -201,6 +205,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
def has_no_punctuation(value: list[str]) -> list[str]:
|
||||
"""Validate result does not contain punctuation."""
|
||||
for sentence in value:
|
||||
# Exclude {list_references} which may contain punctuation characters.
|
||||
sentence = _remove_list_references(sentence)
|
||||
if (
|
||||
PUNCTUATION_START.search(sentence)
|
||||
or PUNCTUATION_END.search(sentence)
|
||||
@@ -212,6 +218,21 @@ def has_no_punctuation(value: list[str]) -> list[str]:
|
||||
return value
|
||||
|
||||
|
||||
def _remove_list_references(sentence: str) -> str:
|
||||
"""Remove {list_references} from a sentence for linting."""
|
||||
return re.sub(r"(?<!\\)\{[^{}]*\}", "", sentence)
|
||||
|
||||
|
||||
def is_valid_sentence(value: list[str]) -> list[str]:
|
||||
"""Validate result can be parsed by hassil."""
|
||||
for sentence in value:
|
||||
try:
|
||||
parse_sentence(sentence)
|
||||
except ParseError as err:
|
||||
raise vol.Invalid(f"invalid sentence: {err}") from err
|
||||
return value
|
||||
|
||||
|
||||
def has_one_non_empty_item(value: list[str]) -> list[str]:
|
||||
"""Validate result has at least one item."""
|
||||
if len(value) < 1:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.6.0"]
|
||||
"requirements": ["hassil==3.7.0"]
|
||||
}
|
||||
|
||||
@@ -24,3 +24,13 @@ OPEN_STATUS: dict[int, str] = {
|
||||
|
||||
LIGHT_MAX_KELVINS = 6500 # 154 Mireds
|
||||
LIGHT_MIN_KELVINS = 2700 # 370 Mireds
|
||||
|
||||
CO2_LEVEL: dict[int, str] = {
|
||||
0: "excellent",
|
||||
1: "good",
|
||||
2: "acceptable",
|
||||
3: "medium",
|
||||
4: "poor",
|
||||
5: "unhealthy",
|
||||
6: "hazardous",
|
||||
}
|
||||
|
||||
@@ -90,10 +90,10 @@ class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
|
||||
|
||||
if feature.has_tilt:
|
||||
self._attr_supported_features |= (
|
||||
CoverEntityFeature.SET_TILT_POSITION
|
||||
| CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT
|
||||
)
|
||||
if feature.is_calibrated:
|
||||
self._attr_supported_features |= CoverEntityFeature.SET_TILT_POSITION
|
||||
|
||||
if feature.tilt_only:
|
||||
self._attr_supported_features &= ~(
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"co2_level": {
|
||||
"default": "mdi:molecule-co2"
|
||||
},
|
||||
"open_status": {
|
||||
"default": "mdi:window-open"
|
||||
},
|
||||
"power_consumption": {
|
||||
"default": "mdi:lightning-bolt"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["blebox_uniapi"],
|
||||
"requirements": ["blebox-uniapi==2.5.4"],
|
||||
"requirements": ["blebox-uniapi==2.5.5"],
|
||||
"zeroconf": ["_bbxsrv._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
UnitOfApparentPower,
|
||||
@@ -31,7 +32,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import BleBoxConfigEntry
|
||||
from .const import OPEN_STATUS
|
||||
from .const import CO2_LEVEL, OPEN_STATUS
|
||||
from .coordinator import BleBoxCoordinator
|
||||
from .entity import BleBoxEntity
|
||||
|
||||
@@ -50,21 +51,25 @@ SENSOR_TYPES: tuple[BleBoxSensorEntityDescription, ...] = (
|
||||
key="pm1",
|
||||
device_class=SensorDeviceClass.PM1,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="pm2_5",
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="pm10",
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="powerConsumption",
|
||||
@@ -76,65 +81,88 @@ SENSOR_TYPES: tuple[BleBoxSensorEntityDescription, ...] = (
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="wind",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="illuminance",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="forwardActiveEnergy",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="reverseActiveEnergy",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="reactivePower",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="activePower",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="apparentPower",
|
||||
device_class=SensorDeviceClass.APPARENT_POWER,
|
||||
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="voltage",
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="current",
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="frequency",
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="openStatus",
|
||||
translation_key="open_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
icon="mdi:window-open",
|
||||
options=list(OPEN_STATUS.values()),
|
||||
value_fn=lambda v: OPEN_STATUS.get(int(v)) if v is not None else None,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="co2",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
BleBoxSensorEntityDescription(
|
||||
key="co2Definition",
|
||||
translation_key="co2_level",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=list(CO2_LEVEL.values()),
|
||||
value_fn=lambda v: CO2_LEVEL.get(int(v)) if v is not None else None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -37,6 +37,18 @@
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"co2_level": {
|
||||
"name": "Carbon dioxide level",
|
||||
"state": {
|
||||
"acceptable": "Acceptable",
|
||||
"excellent": "Excellent",
|
||||
"good": "Good",
|
||||
"hazardous": "Hazardous",
|
||||
"medium": "Medium",
|
||||
"poor": "Poor",
|
||||
"unhealthy": "Unhealthy"
|
||||
}
|
||||
},
|
||||
"open_status": {
|
||||
"state": {
|
||||
"ajar": "Ajar",
|
||||
|
||||
@@ -169,9 +169,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
|
||||
try:
|
||||
await self._camera.save_recent_clips(output_dir=file_path)
|
||||
except OSError as err:
|
||||
# pylint: disable-next=home-assistant-exception-message-with-translation
|
||||
raise ServiceValidationError(
|
||||
str(err),
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cant_write",
|
||||
) from err
|
||||
@@ -191,9 +189,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
|
||||
try:
|
||||
await self._camera.video_to_file(filename)
|
||||
except OSError as err:
|
||||
# pylint: disable-next=home-assistant-exception-message-with-translation
|
||||
raise ServiceValidationError(
|
||||
str(err),
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cant_write",
|
||||
) from err
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"cant_write": {
|
||||
"message": "Can't write to file."
|
||||
"message": "Can't write to file, check logs for details."
|
||||
},
|
||||
"failed_arm": {
|
||||
"message": "Blink failed to arm camera."
|
||||
|
||||
@@ -6,6 +6,7 @@ These APIs are the only documented way to interact with the bluetooth integratio
|
||||
import asyncio
|
||||
from asyncio import Future
|
||||
from collections.abc import Callable, Iterable
|
||||
from contextlib import ExitStack
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from bleak import BleakScanner
|
||||
@@ -178,15 +179,20 @@ async def async_process_advertisements(
|
||||
if not done.done() and callback(service_info):
|
||||
done.set_result(service_info)
|
||||
|
||||
unload = _get_manager(hass).async_register_callback(
|
||||
_async_discovered_device, match_dict, mode, scan_duration=timeout
|
||||
)
|
||||
manager = _get_manager(hass)
|
||||
|
||||
with ExitStack() as stack:
|
||||
unload = manager.async_register_callback(
|
||||
_async_discovered_device, match_dict, mode
|
||||
)
|
||||
stack.callback(unload)
|
||||
|
||||
if mode == BluetoothScanningMode.ACTIVE:
|
||||
task = hass.async_create_task(manager.async_request_active_scan(timeout))
|
||||
stack.callback(task.cancel)
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(timeout):
|
||||
return await done
|
||||
finally:
|
||||
unload()
|
||||
|
||||
|
||||
@hass_callback
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.6.4",
|
||||
"bluetooth-data-tools==1.29.18",
|
||||
"dbus-fast==5.0.16",
|
||||
"habluetooth==6.8.1"
|
||||
"habluetooth==6.8.3"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -230,10 +230,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) ->
|
||||
entry.minor_version,
|
||||
)
|
||||
|
||||
if entry.version > 1:
|
||||
# Downgraded from a future version; cannot migrate.
|
||||
return False
|
||||
|
||||
# 1.1 -> 1.2: Add CONF_HEATING_CIRCUITS. Attempt to discover available
|
||||
# heating circuits from the device; fall back to [1] (pre-multi-circuit
|
||||
# default) if the device is unreachable or the endpoint is unsupported.
|
||||
|
||||
@@ -183,7 +183,6 @@ class BSBLANClimate(BSBLanCircuitEntity, ClimateEntity):
|
||||
try:
|
||||
await self.coordinator.client.thermostat(**data, circuit=self._circuit)
|
||||
except BSBLANError as err:
|
||||
# pylint: disable-next=home-assistant-exception-message-with-translation
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_data_error",
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiostreammagic"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiostreammagic==2.13.1"],
|
||||
"requirements": ["aiostreammagic==2.13.2"],
|
||||
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/chacon_dio",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["dio_chacon_api"],
|
||||
"requirements": ["dio-chacon-wifi-api==1.2.2"]
|
||||
"requirements": ["dio-chacon-wifi-api==1.3.0"]
|
||||
}
|
||||
|
||||
@@ -87,10 +87,6 @@ async def async_migrate_entry(
|
||||
) -> bool:
|
||||
"""Migrate old entry."""
|
||||
|
||||
if config_entry.version > 1:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if config_entry.version == 1 and config_entry.minor_version == 1:
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.6.0", "home-assistant-intents==2026.6.1"]
|
||||
"requirements": ["hassil==3.7.0", "home-assistant-intents==2026.6.1"]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Offer sentence based automation rules."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from hassil.parse_expression import parse_sentence
|
||||
from hassil.parser import ParseError
|
||||
from hassil.recognize import RecognizeResult
|
||||
from hassil.util import (
|
||||
PUNCTUATION_END,
|
||||
@@ -31,6 +34,8 @@ TRIGGER_CALLBACK_TYPE = Callable[
|
||||
def has_no_punctuation(value: list[str]) -> list[str]:
|
||||
"""Validate result does not contain punctuation."""
|
||||
for sentence in value:
|
||||
# Exclude {list_references} which may contain punctuation characters.
|
||||
sentence = _remove_list_references(sentence)
|
||||
if (
|
||||
PUNCTUATION_START.search(sentence)
|
||||
or PUNCTUATION_END.search(sentence)
|
||||
@@ -42,6 +47,21 @@ def has_no_punctuation(value: list[str]) -> list[str]:
|
||||
return value
|
||||
|
||||
|
||||
def _remove_list_references(sentence: str) -> str:
|
||||
"""Remove {list_references} from a sentence for linting."""
|
||||
return re.sub(r"(?<!\\)\{[^{}]*\}", "", sentence)
|
||||
|
||||
|
||||
def is_valid_sentence(value: list[str]) -> list[str]:
|
||||
"""Validate result can be parsed by hassil."""
|
||||
for sentence in value:
|
||||
try:
|
||||
parse_sentence(sentence)
|
||||
except ParseError as err:
|
||||
raise vol.Invalid(f"invalid sentence: {err}") from err
|
||||
return value
|
||||
|
||||
|
||||
def has_one_non_empty_item(value: list[str]) -> list[str]:
|
||||
"""Validate result has at least one item."""
|
||||
if len(value) < 1:
|
||||
@@ -58,7 +78,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): DOMAIN,
|
||||
vol.Required(CONF_COMMAND): vol.All(
|
||||
cv.ensure_list, [cv.string], has_one_non_empty_item, has_no_punctuation
|
||||
cv.ensure_list,
|
||||
[cv.string],
|
||||
has_one_non_empty_item,
|
||||
has_no_punctuation,
|
||||
is_valid_sentence,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"requirements": ["pydaikin==2.17.2"],
|
||||
"requirements": ["pydaikin==2.18.1"],
|
||||
"zeroconf": ["_dkapi._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from data_grand_lyon_ha import DataGrandLyonClient
|
||||
from data_grand_lyon_ha import DataGrandLyonClient, TclStop, find_tcl_stop_by_id
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
@@ -18,6 +18,12 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_LINE,
|
||||
@@ -43,13 +49,6 @@ STEP_RECONFIGURE_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
STEP_STOP_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_LINE): str,
|
||||
vol.Required(CONF_STOP_ID): vol.Coerce(int),
|
||||
}
|
||||
)
|
||||
|
||||
STEP_VELOV_STATION_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_STATION_ID): vol.Coerce(int),
|
||||
@@ -179,33 +178,126 @@ class DataGrandLyonConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class StopSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Handle a subentry flow for adding a Data Grand Lyon stop."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the flow."""
|
||||
self._stops: list[TclStop] = []
|
||||
self._selected_stop: TclStop | None = None
|
||||
self._selected_stop_id: int | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Handle the user step to add a new stop."""
|
||||
entry = self._get_entry()
|
||||
"""Pick a stop from the list fetched from the API, or enter one manually."""
|
||||
if not self._stops:
|
||||
if error := await self._async_load_stops():
|
||||
return self.async_abort(reason=error)
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
line = user_input[CONF_LINE]
|
||||
stop_id = user_input[CONF_STOP_ID]
|
||||
unique_id = f"{line}_{stop_id}"
|
||||
try:
|
||||
stop_id = int(user_input[CONF_STOP_ID])
|
||||
except ValueError:
|
||||
errors[CONF_STOP_ID] = "invalid_stop_id"
|
||||
else:
|
||||
self._selected_stop_id = stop_id
|
||||
self._selected_stop = find_tcl_stop_by_id(self._stops, stop_id)
|
||||
return await self.async_step_pick_line()
|
||||
|
||||
for subentry in entry.subentries.values():
|
||||
if subentry.unique_id == unique_id:
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
name = f"{line} - Stop {stop_id}"
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data={CONF_LINE: line, CONF_STOP_ID: stop_id},
|
||||
unique_id=unique_id,
|
||||
options = [
|
||||
SelectOptionDict(value=str(stop.id), label=_stop_label(stop))
|
||||
for stop in sorted(
|
||||
self._stops, key=lambda s: (s.nom, s.commune or "", s.id or 0)
|
||||
)
|
||||
|
||||
]
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_STOP_ID): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=options,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
sort=False,
|
||||
custom_value=True,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_STOP_DATA_SCHEMA,
|
||||
data_schema=schema,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_pick_line(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Pick a line from the selected stop's desserte, or enter one manually."""
|
||||
assert self._selected_stop_id is not None
|
||||
if user_input is not None:
|
||||
return self._create_stop(
|
||||
line=user_input[CONF_LINE], stop_id=self._selected_stop_id
|
||||
)
|
||||
|
||||
options = self._selected_stop.desserte if self._selected_stop else []
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_LINE): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=options,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
custom_value=True,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
return self.async_show_form(step_id="pick_line", data_schema=schema)
|
||||
|
||||
async def _async_load_stops(self) -> str | None:
|
||||
"""Fetch TCL stops from the API, returning an error key on failure."""
|
||||
entry = self._get_entry()
|
||||
session = async_get_clientsession(self.hass)
|
||||
client = DataGrandLyonClient(
|
||||
session=session,
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
)
|
||||
try:
|
||||
self._stops = await client.get_tcl_stops()
|
||||
except ClientResponseError as err:
|
||||
if err.status in (401, 403):
|
||||
return "invalid_auth"
|
||||
return "cannot_connect"
|
||||
except ClientError, TimeoutError:
|
||||
return "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error fetching Data Grand Lyon TCL stops")
|
||||
return "unknown"
|
||||
return None
|
||||
|
||||
def _create_stop(self, line: str, stop_id: int) -> SubentryFlowResult:
|
||||
"""Create the stop subentry, aborting on duplicate."""
|
||||
entry = self._get_entry()
|
||||
unique_id = f"{line}_{stop_id}"
|
||||
for subentry in entry.subentries.values():
|
||||
if subentry.unique_id == unique_id:
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"{line} - Stop {stop_id}",
|
||||
data={CONF_LINE: line, CONF_STOP_ID: stop_id},
|
||||
unique_id=unique_id,
|
||||
)
|
||||
|
||||
|
||||
def _stop_label(stop: TclStop) -> str:
|
||||
label = stop.nom
|
||||
# variable extracted to please codespell.
|
||||
address = stop.adresse # codespell:ignore adresse
|
||||
if address or stop.commune:
|
||||
label += " (" + ", ".join(filter(None, [address, stop.commune])) + ")"
|
||||
label += f" - {stop.id}"
|
||||
|
||||
return label
|
||||
|
||||
|
||||
class VelovStationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Handle a subentry flow for adding a Vélo'v station."""
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["data-grand-lyon-ha==0.7.0"]
|
||||
"requirements": ["data-grand-lyon-ha==0.8.0"]
|
||||
}
|
||||
|
||||
@@ -46,17 +46,30 @@
|
||||
"config_subentries": {
|
||||
"stop": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"entry_type": "Transit stop",
|
||||
"error": {
|
||||
"invalid_stop_id": "Stop ID must be a number."
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "Add transit stop"
|
||||
},
|
||||
"step": {
|
||||
"pick_line": {
|
||||
"data": {
|
||||
"line": "Line"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"line": "Line",
|
||||
"stop_id": "Stop ID"
|
||||
"stop_id": "Stop"
|
||||
},
|
||||
"data_description": {
|
||||
"stop_id": "Search by stop name, address or city, or enter a stop ID directly."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -27,7 +28,19 @@ async def async_setup_entry(
|
||||
BinarySensorDeviceClass.MOISTURE,
|
||||
),
|
||||
DemoBinarySensor(
|
||||
"binary_2", "Movement Backyard", True, BinarySensorDeviceClass.MOTION
|
||||
"binary_2",
|
||||
"Movement Backyard",
|
||||
True,
|
||||
BinarySensorDeviceClass.MOTION,
|
||||
),
|
||||
DemoBinarySensor(
|
||||
"binary_3",
|
||||
"Outside Temperature",
|
||||
False,
|
||||
BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
device_id="sensor_1",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_name="Battery Charging",
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -46,6 +59,9 @@ class DemoBinarySensor(BinarySensorEntity):
|
||||
device_name: str,
|
||||
state: bool,
|
||||
device_class: BinarySensorDeviceClass,
|
||||
device_id: str | None = None,
|
||||
entity_category: EntityCategory | None = None,
|
||||
entity_name: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the demo sensor."""
|
||||
self._unique_id = unique_id
|
||||
@@ -54,10 +70,12 @@ class DemoBinarySensor(BinarySensorEntity):
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
# Serial numbers are unique identifiers within a specific domain
|
||||
(DOMAIN, self.unique_id)
|
||||
(DOMAIN, device_id or unique_id)
|
||||
},
|
||||
name=device_name,
|
||||
)
|
||||
self._attr_entity_category = entity_category
|
||||
self._attr_name = entity_name
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
|
||||
@@ -54,10 +54,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
config_entry.minor_version,
|
||||
)
|
||||
|
||||
if config_entry.version > 1:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if config_entry.version == 1:
|
||||
if config_entry.minor_version < 2:
|
||||
new_options = {**config_entry.options}
|
||||
|
||||
@@ -22,6 +22,7 @@ from .const import ( # noqa: F401
|
||||
ATTR_LOCATION_NAME,
|
||||
ATTR_MAC,
|
||||
ATTR_SOURCE_TYPE,
|
||||
ATTR_TRACKING_TYPE,
|
||||
CONF_ASSOCIATED_ZONE,
|
||||
CONF_CONSIDER_HOME,
|
||||
CONF_NEW_DEVICE_DEFAULTS,
|
||||
@@ -36,6 +37,7 @@ from .const import ( # noqa: F401
|
||||
PLATFORM_TYPE_LEGACY,
|
||||
SCAN_INTERVAL,
|
||||
SourceType,
|
||||
TrackingType,
|
||||
)
|
||||
from .entity import ( # noqa: F401
|
||||
BaseScannerEntity,
|
||||
|
||||
@@ -25,6 +25,18 @@ class SourceType(StrEnum):
|
||||
BLUETOOTH_LE = "bluetooth_le"
|
||||
|
||||
|
||||
class TrackingType(StrEnum):
|
||||
"""Tracking type for device trackers.
|
||||
|
||||
Describes how the tracker determines presence: by the device's geographic
|
||||
position (e.g. GPS) or by its connection to a known endpoint (e.g. a router
|
||||
or beacon associated with a zone).
|
||||
"""
|
||||
|
||||
CONNECTION = "connection"
|
||||
POSITION = "position"
|
||||
|
||||
|
||||
CONF_SCAN_INTERVAL: Final = "interval_seconds"
|
||||
SCAN_INTERVAL: Final = timedelta(seconds=12)
|
||||
|
||||
@@ -47,6 +59,7 @@ ATTR_IN_ZONES: Final = "in_zones"
|
||||
ATTR_LOCATION_NAME: Final = "location_name"
|
||||
ATTR_MAC: Final = "mac"
|
||||
ATTR_SOURCE_TYPE: Final = "source_type"
|
||||
ATTR_TRACKING_TYPE: Final = "tracking_type"
|
||||
ATTR_CONSIDER_HOME: Final = "consider_home"
|
||||
ATTR_IP: Final = "ip"
|
||||
|
||||
|
||||
@@ -48,11 +48,13 @@ from .const import (
|
||||
ATTR_IP,
|
||||
ATTR_MAC,
|
||||
ATTR_SOURCE_TYPE,
|
||||
ATTR_TRACKING_TYPE,
|
||||
CONF_ASSOCIATED_ZONE,
|
||||
CONNECTED_DEVICE_REGISTERED,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
SourceType,
|
||||
TrackingType,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -238,6 +240,9 @@ class TrackerEntity(
|
||||
"""Base class for a tracked device."""
|
||||
|
||||
entity_description: TrackerEntityDescription
|
||||
_attr_capability_attributes: dict[str, Any] = {
|
||||
ATTR_TRACKING_TYPE: TrackingType.POSITION
|
||||
}
|
||||
_attr_in_zones: list[str] | None = None
|
||||
_attr_latitude: float | None = None
|
||||
_attr_location_accuracy: float = 0
|
||||
@@ -411,6 +416,9 @@ class BaseScannerEntity(BaseTrackerEntity):
|
||||
addresses being used to identify the device.
|
||||
"""
|
||||
|
||||
_attr_capability_attributes: dict[str, Any] = {
|
||||
ATTR_TRACKING_TYPE: TrackingType.CONNECTION
|
||||
}
|
||||
_scanner_option_associated_zone: str = zone.ENTITY_ID_HOME
|
||||
_scanner_option_associated_zone_unsub: CALLBACK_TYPE | None = None
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@
|
||||
"gps": "GPS",
|
||||
"router": "Router"
|
||||
}
|
||||
},
|
||||
"tracking_type": {
|
||||
"name": "Tracking type",
|
||||
"state": {
|
||||
"connection": "Connection",
|
||||
"position": "Position"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DnsIPConfigEntry) -> boo
|
||||
tcp_port=entry.options[CONF_PORT],
|
||||
udp_port=entry.options[CONF_PORT],
|
||||
)
|
||||
queries.append(resolver_ipv4.query(hostname, "A"))
|
||||
queries.append(resolver_ipv4.query_dns(hostname, "A"))
|
||||
|
||||
if entry.data[CONF_IPV6]:
|
||||
resolver_ipv6 = aiodns.DNSResolver(
|
||||
@@ -59,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DnsIPConfigEntry) -> boo
|
||||
tcp_port=entry.options[CONF_PORT_IPV6],
|
||||
udp_port=entry.options[CONF_PORT_IPV6],
|
||||
)
|
||||
queries.append(resolver_ipv6.query(hostname, "AAAA"))
|
||||
queries.append(resolver_ipv6.query_dns(hostname, "AAAA"))
|
||||
|
||||
async def _close_resolvers() -> None:
|
||||
if resolver_ipv4 is not None:
|
||||
@@ -111,10 +111,6 @@ async def async_migrate_entry(
|
||||
) -> bool:
|
||||
"""Migrate old entry to a newer version."""
|
||||
|
||||
if config_entry.version > 1:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if config_entry.version < 2 and config_entry.minor_version < 2:
|
||||
_LOGGER.debug(
|
||||
"Migrating configuration from version %s.%s",
|
||||
|
||||
@@ -72,7 +72,7 @@ async def async_validate_hostname(
|
||||
_resolver = aiodns.DNSResolver(
|
||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||
)
|
||||
result = bool(await _resolver.query(hostname, qtype))
|
||||
result = bool(await _resolver.query_dns(hostname, qtype))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Literal
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
import pycares
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.const import CONF_NAME, CONF_PORT
|
||||
@@ -148,7 +149,7 @@ class WanIpSensor(SensorEntity):
|
||||
response = None
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
response = await self._resolver.query(self.hostname, self.querytype)
|
||||
response = await self._resolver.query_dns(self.hostname, self.querytype)
|
||||
except TimeoutError as err:
|
||||
_LOGGER.debug("Timeout while resolving host: %s", err)
|
||||
await self._resolver.close()
|
||||
@@ -157,9 +158,19 @@ class WanIpSensor(SensorEntity):
|
||||
await self._resolver.close()
|
||||
|
||||
if response:
|
||||
sorted_ips = sort_ips(
|
||||
[res.host for res in response], querytype=self.querytype
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
assert all(
|
||||
isinstance(res.data, (pycares.ARecordData, pycares.AAAARecordData))
|
||||
for res in response.answer
|
||||
)
|
||||
_ips = []
|
||||
for res in response.answer:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(
|
||||
res.data, (pycares.ARecordData, pycares.AAAARecordData)
|
||||
)
|
||||
_ips.append(res.data.addr)
|
||||
sorted_ips = sort_ips(_ips, querytype=self.querytype)
|
||||
self._attr_native_value = sorted_ips[0]
|
||||
self._attr_extra_state_attributes["ip_addresses"] = sorted_ips
|
||||
self._attr_available = True
|
||||
|
||||
@@ -13,7 +13,6 @@ from dsmr_parser.clients.rfxtrx_protocol import (
|
||||
from dsmr_parser.objects import DSMRObject
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import usb
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
@@ -23,6 +22,7 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.selector import SerialPortSelector
|
||||
|
||||
from .const import (
|
||||
CONF_DSMR_VERSION,
|
||||
@@ -37,8 +37,6 @@ from .const import (
|
||||
RFXTRX_DSMR_PROTOCOL,
|
||||
)
|
||||
|
||||
CONF_MANUAL_PATH = "Enter Manually"
|
||||
|
||||
|
||||
class DSMRConnection:
|
||||
"""Test the connection to DSMR and receive telegram to read serial ids."""
|
||||
@@ -165,8 +163,6 @@ class DSMRFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_dsmr_version: str | None = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
@@ -222,34 +218,13 @@ class DSMRFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Step when setting up serial configuration."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
user_selection = user_input[CONF_PORT]
|
||||
if user_selection == CONF_MANUAL_PATH:
|
||||
self._dsmr_version = user_input[CONF_DSMR_VERSION]
|
||||
return await self.async_step_setup_serial_manual_path()
|
||||
|
||||
dev_path = user_selection
|
||||
|
||||
validate_data = {
|
||||
CONF_PORT: dev_path,
|
||||
CONF_DSMR_VERSION: user_input[CONF_DSMR_VERSION],
|
||||
}
|
||||
|
||||
data = await self.async_validate_dsmr(validate_data, errors)
|
||||
data = await self.async_validate_dsmr(user_input, errors)
|
||||
if not errors:
|
||||
return self.async_create_entry(title=data[CONF_PORT], data=data)
|
||||
|
||||
ports = await usb.async_scan_serial_ports(self.hass)
|
||||
list_of_ports = {
|
||||
port.device: f"{port.device} - {port.description or 'n/a'}"
|
||||
f", s/n: {port.serial_number or 'n/a'}"
|
||||
+ (f" - {port.manufacturer}" if port.manufacturer else "")
|
||||
for port in ports
|
||||
}
|
||||
list_of_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PORT): vol.In(list_of_ports),
|
||||
vol.Required(CONF_PORT): SerialPortSelector(),
|
||||
vol.Required(CONF_DSMR_VERSION): vol.In(DSMR_VERSIONS),
|
||||
}
|
||||
)
|
||||
@@ -259,27 +234,6 @@ class DSMRFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_setup_serial_manual_path(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Select path manually."""
|
||||
if user_input is not None:
|
||||
validate_data = {
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
CONF_DSMR_VERSION: self._dsmr_version,
|
||||
}
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
data = await self.async_validate_dsmr(validate_data, errors)
|
||||
if not errors:
|
||||
return self.async_create_entry(title=data[CONF_PORT], data=data)
|
||||
|
||||
schema = vol.Schema({vol.Required(CONF_PORT): str})
|
||||
return self.async_show_form(
|
||||
step_id="setup_serial_manual_path",
|
||||
data_schema=schema,
|
||||
)
|
||||
|
||||
async def async_validate_dsmr(
|
||||
self, input_data: dict[str, Any], errors: dict[str, str]
|
||||
) -> dict[str, Any]:
|
||||
|
||||
@@ -26,12 +26,6 @@
|
||||
},
|
||||
"title": "[%key:common::config_flow::data::device%]"
|
||||
},
|
||||
"setup_serial_manual_path": {
|
||||
"data": {
|
||||
"port": "[%key:common::config_flow::data::usb_path%]"
|
||||
},
|
||||
"title": "[%key:common::config_flow::data::path%]"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"type": "Connection type"
|
||||
|
||||
@@ -7,3 +7,4 @@ from homeassistant.const import Platform
|
||||
DOMAIN = "duco"
|
||||
PLATFORMS = [Platform.FAN, Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
BOX_NODE_ID = 1
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from duco_connectivity.exceptions import DucoConnectionError
|
||||
from duco_connectivity.exceptions import DucoConnectionError, DucoError
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_HOST
|
||||
@@ -52,6 +52,12 @@ async def async_get_config_entry_diagnostics(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
) from err
|
||||
except DucoError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
api_info: dict[str, Any] = {"public_api_version": api_info_obj.public_api_version}
|
||||
if api_info_obj.reported_api_version is not None:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Base entity for the Duco integration."""
|
||||
|
||||
from duco_connectivity.models import Node
|
||||
from duco_connectivity.models import Node, NodeType
|
||||
|
||||
from homeassistant.const import ATTR_VIA_DEVICE
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
@@ -25,7 +25,7 @@ class DucoEntity(CoordinatorEntity[DucoCoordinator]):
|
||||
identifiers={(DOMAIN, f"{mac}_{node.node_id}")},
|
||||
manufacturer="Duco",
|
||||
model=coordinator.board_info.box_name
|
||||
if node.general.node_type == "BOX"
|
||||
if node.general.node_type == NodeType.BOX
|
||||
else node.general.node_type,
|
||||
name=node.general.name or f"Node {node.node_id}",
|
||||
)
|
||||
@@ -34,7 +34,7 @@ class DucoEntity(CoordinatorEntity[DucoCoordinator]):
|
||||
"connections": {(CONNECTION_NETWORK_MAC, mac)},
|
||||
"serial_number": coordinator.board_info.serial_board_box,
|
||||
}
|
||||
if node.general.node_type == "BOX"
|
||||
if node.general.node_type == NodeType.BOX
|
||||
else {ATTR_VIA_DEVICE: (DOMAIN, f"{mac}_1")}
|
||||
)
|
||||
self._attr_device_info = device_info
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import BOX_NODE_ID, DOMAIN
|
||||
from .coordinator import DucoConfigEntry, DucoCoordinator
|
||||
from .entity import DucoEntity
|
||||
|
||||
@@ -158,7 +158,13 @@ async def async_setup_entry(
|
||||
# The firmware removes deregistered RF/wired nodes automatically.
|
||||
# BSRH box sensors that are physically unplugged from the PCB are
|
||||
# not deregistered by the firmware and will never appear here as stale.
|
||||
stale_node_ids = known_nodes - coordinator.data.nodes.keys()
|
||||
# The BOX node can transiently disappear from the API response, so keep
|
||||
# node 1 to avoid removing the main controller device.
|
||||
stale_node_ids = {
|
||||
node_id
|
||||
for node_id in known_nodes - coordinator.data.nodes.keys()
|
||||
if node_id != BOX_NODE_ID
|
||||
}
|
||||
if stale_node_ids:
|
||||
device_reg = dr.async_get(hass)
|
||||
mac = entry.unique_id
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"name": "Target flow level"
|
||||
},
|
||||
"time_state_end": {
|
||||
"name": "Mode end time"
|
||||
"name": "State end time"
|
||||
},
|
||||
"ventilation_state": {
|
||||
"name": "Ventilation state",
|
||||
|
||||
@@ -9,7 +9,6 @@ Warnungen vor markantem Wetter (Stufe 2) # codespell:ignore vor
|
||||
Wetterwarnungen (Stufe 1)
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
@@ -17,6 +16,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ADVANCE_WARNING_SENSOR,
|
||||
@@ -100,7 +100,7 @@ class DwdWeatherWarningsSensor(
|
||||
if warnings is None:
|
||||
return []
|
||||
|
||||
now = datetime.now(UTC) # pylint: disable=home-assistant-enforce-utcnow
|
||||
now = dt_util.utcnow()
|
||||
return [warning for warning in warnings if warning[API_ATTR_WARNING_END] > now]
|
||||
|
||||
@property
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyecobee"],
|
||||
"requirements": ["python-ecobee-api==0.4.0"],
|
||||
"requirements": ["python-ecobee-api==0.4.1"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": [
|
||||
{
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sml"],
|
||||
"requirements": ["pysml==0.1.7"]
|
||||
"requirements": ["pysml==0.1.8"]
|
||||
}
|
||||
|
||||
@@ -98,10 +98,6 @@ async def async_migrate_entry(
|
||||
config_entry.minor_version,
|
||||
)
|
||||
|
||||
if config_entry.version > 1:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if config_entry.version == 1:
|
||||
new_options = {**config_entry.options}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_track_time_interval,
|
||||
)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
CONF_DEVICE_NAME,
|
||||
@@ -221,8 +222,7 @@ def update_listeners(hass: HomeAssistant, entry: EnergyIDConfigEntry) -> None:
|
||||
):
|
||||
try:
|
||||
value = float(current_state.state)
|
||||
# pylint: disable-next=home-assistant-enforce-utcnow
|
||||
timestamp = current_state.last_updated or dt.datetime.now(dt.UTC)
|
||||
timestamp = current_state.last_updated or dt_util.utcnow()
|
||||
client.get_or_create_sensor(energyid_key).update(value, timestamp)
|
||||
except ValueError, TypeError:
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -8,9 +8,25 @@ from env_canada import ECAirQuality, ECMap, ECWeather
|
||||
from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_STATION
|
||||
from .const import (
|
||||
CONF_RADAR_LAYER,
|
||||
CONF_RADAR_LEGEND,
|
||||
CONF_RADAR_OPACITY,
|
||||
CONF_RADAR_RADIUS,
|
||||
CONF_RADAR_TIMESTAMP,
|
||||
CONF_STATION,
|
||||
DEFAULT_RADAR_LAYER,
|
||||
DEFAULT_RADAR_LEGEND,
|
||||
DEFAULT_RADAR_OPACITY,
|
||||
DEFAULT_RADAR_RADIUS,
|
||||
DEFAULT_RADAR_TIMESTAMP,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import ECConfigEntry, ECDataUpdateCoordinator, ECRuntimeData
|
||||
from .services import async_setup_services
|
||||
|
||||
DEFAULT_RADAR_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
DEFAULT_WEATHER_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
@@ -19,6 +35,14 @@ PLATFORMS = [Platform.CAMERA, Platform.SENSOR, Platform.WEATHER]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Environment Canada services."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ECConfigEntry) -> bool:
|
||||
"""Set up EC as config entry."""
|
||||
@@ -43,7 +67,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ECConfigEntry) ->
|
||||
errors = errors + 1
|
||||
_LOGGER.warning("Unable to retrieve Environment Canada weather")
|
||||
|
||||
radar_data = ECMap(coordinates=(lat, lon), layer="precip_type", legend=False)
|
||||
options = config_entry.options
|
||||
radar_data = ECMap(
|
||||
coordinates=(lat, lon),
|
||||
layer=options.get(CONF_RADAR_LAYER, DEFAULT_RADAR_LAYER),
|
||||
legend=options.get(CONF_RADAR_LEGEND, DEFAULT_RADAR_LEGEND),
|
||||
timestamp=options.get(CONF_RADAR_TIMESTAMP, DEFAULT_RADAR_TIMESTAMP),
|
||||
layer_opacity=int(options.get(CONF_RADAR_OPACITY, DEFAULT_RADAR_OPACITY)),
|
||||
radius=int(options.get(CONF_RADAR_RADIUS, DEFAULT_RADAR_RADIUS)),
|
||||
)
|
||||
radar_coordinator = ECDataUpdateCoordinator(
|
||||
hass, config_entry, radar_data, "radar", DEFAULT_RADAR_UPDATE_INTERVAL
|
||||
)
|
||||
|
||||
@@ -9,17 +9,42 @@ from env_canada import ECWeather, ec_exc
|
||||
from env_canada.ec_weather import get_ec_sites_list
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithReload,
|
||||
)
|
||||
from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
NumberSelector,
|
||||
NumberSelectorConfig,
|
||||
NumberSelectorMode,
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_STATION, CONF_TITLE, DOMAIN
|
||||
from .const import (
|
||||
CONF_RADAR_LAYER,
|
||||
CONF_RADAR_LEGEND,
|
||||
CONF_RADAR_OPACITY,
|
||||
CONF_RADAR_RADIUS,
|
||||
CONF_RADAR_TIMESTAMP,
|
||||
CONF_STATION,
|
||||
CONF_TITLE,
|
||||
DEFAULT_RADAR_LAYER,
|
||||
DEFAULT_RADAR_LEGEND,
|
||||
DEFAULT_RADAR_OPACITY,
|
||||
DEFAULT_RADAR_RADIUS,
|
||||
DEFAULT_RADAR_TIMESTAMP,
|
||||
DOMAIN,
|
||||
RADAR_LAYERS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,6 +82,14 @@ class EnvironmentCanadaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
_station_codes: list[dict[str, str]] | None = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Return the options flow handler."""
|
||||
return OptionsFlowHandler()
|
||||
|
||||
async def _get_station_codes(self) -> list[dict[str, str]]:
|
||||
"""Get station codes, cached after first call."""
|
||||
if self._station_codes is None:
|
||||
@@ -127,3 +160,55 @@ class EnvironmentCanadaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=data_schema, errors=errors
|
||||
)
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
"""Handle Environment Canada radar camera options."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the radar camera options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(data=user_input)
|
||||
|
||||
options = self.config_entry.options
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_RADAR_LAYER,
|
||||
default=options.get(CONF_RADAR_LAYER, DEFAULT_RADAR_LAYER),
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=RADAR_LAYERS,
|
||||
translation_key="radar_layer",
|
||||
)
|
||||
),
|
||||
vol.Required(
|
||||
CONF_RADAR_LEGEND,
|
||||
default=options.get(CONF_RADAR_LEGEND, DEFAULT_RADAR_LEGEND),
|
||||
): BooleanSelector(),
|
||||
vol.Required(
|
||||
CONF_RADAR_TIMESTAMP,
|
||||
default=options.get(CONF_RADAR_TIMESTAMP, DEFAULT_RADAR_TIMESTAMP),
|
||||
): BooleanSelector(),
|
||||
vol.Required(
|
||||
CONF_RADAR_OPACITY,
|
||||
default=options.get(CONF_RADAR_OPACITY, DEFAULT_RADAR_OPACITY),
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=0, max=100, step=1, mode=NumberSelectorMode.SLIDER
|
||||
)
|
||||
),
|
||||
vol.Required(
|
||||
CONF_RADAR_RADIUS,
|
||||
default=options.get(CONF_RADAR_RADIUS, DEFAULT_RADAR_RADIUS),
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=10, max=2000, step=10, unit_of_measurement="km"
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||
|
||||
@@ -6,3 +6,19 @@ CONF_STATION = "station"
|
||||
CONF_TITLE = "title"
|
||||
DOMAIN = "environment_canada"
|
||||
SERVICE_ENVIRONMENT_CANADA_FORECASTS = "get_forecasts"
|
||||
|
||||
CONF_RADAR_LAYER = "radar_layer"
|
||||
CONF_RADAR_LEGEND = "radar_legend"
|
||||
CONF_RADAR_TIMESTAMP = "radar_timestamp"
|
||||
CONF_RADAR_OPACITY = "radar_opacity"
|
||||
CONF_RADAR_RADIUS = "radar_radius"
|
||||
|
||||
RADAR_LAYERS = ["rain", "snow", "precip_type"]
|
||||
|
||||
# Defaults preserve the radar behaviour from before the options flow existed:
|
||||
# the precipitation-type layer with the legend hidden.
|
||||
DEFAULT_RADAR_LAYER = "precip_type"
|
||||
DEFAULT_RADAR_LEGEND = False
|
||||
DEFAULT_RADAR_TIMESTAMP = True
|
||||
DEFAULT_RADAR_OPACITY = 65
|
||||
DEFAULT_RADAR_RADIUS = 200
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_alerts": {
|
||||
"service": "mdi:bell-alert"
|
||||
},
|
||||
"get_forecasts": {
|
||||
"service": "mdi:weather-cloudy-clock"
|
||||
},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.13.2"]
|
||||
"requirements": ["env-canada==0.15.0"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Define services for the Environment Canada integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from env_canada import ECWeather
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
SERVICE_GET_ALERTS = "get_alerts"
|
||||
SERVICE_GET_ALERTS_SCHEMA = vol.Schema({vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string})
|
||||
|
||||
SNAKE_MAPPING = {
|
||||
"alertColourLevel": "alert_colour_level",
|
||||
"expiryTime": "expiry_time",
|
||||
}
|
||||
|
||||
|
||||
async def _async_get_alerts(call: ServiceCall) -> dict[str, Any]:
|
||||
"""Return the active alerts."""
|
||||
entry = service.async_get_config_entry(
|
||||
call.hass, DOMAIN, call.data[ATTR_CONFIG_ENTRY_ID]
|
||||
)
|
||||
|
||||
ec: ECWeather | None = entry.runtime_data.weather_coordinator.ec_data
|
||||
if ec is None:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_connected",
|
||||
)
|
||||
|
||||
data: dict[str, Any] = ec.alerts
|
||||
return {
|
||||
k: [
|
||||
{SNAKE_MAPPING.get(ik, ik): iv for ik, iv in item.items()}
|
||||
for item in v["value"]
|
||||
]
|
||||
for k, v in data.items()
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Environment Canada integration."""
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GET_ALERTS,
|
||||
_async_get_alerts,
|
||||
schema=SERVICE_GET_ALERTS_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
@@ -1,3 +1,11 @@
|
||||
get_alerts:
|
||||
fields:
|
||||
config_entry_id:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: environment_canada
|
||||
|
||||
get_forecasts:
|
||||
target:
|
||||
entity:
|
||||
|
||||
@@ -112,7 +112,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"not_connected": {
|
||||
"message": "Environment Canada is not connected"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"radar_layer": "Radar type",
|
||||
"radar_legend": "Show legend",
|
||||
"radar_opacity": "Radar opacity",
|
||||
"radar_radius": "Map radius",
|
||||
"radar_timestamp": "Show timestamp"
|
||||
},
|
||||
"data_description": {
|
||||
"radar_opacity": "Opacity of the radar layer overlay (0-100)",
|
||||
"radar_radius": "Radius of the radar map in kilometres"
|
||||
},
|
||||
"title": "Radar camera options"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"radar_layer": {
|
||||
"options": {
|
||||
"precip_type": "Precipitation type",
|
||||
"rain": "Rain",
|
||||
"snow": "Snow"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_alerts": {
|
||||
"description": "Retrieves the alerts from the selected weather service.",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"description": "The Environment Canada service to retrieve alerts from.",
|
||||
"name": "Environment Canada service"
|
||||
}
|
||||
},
|
||||
"name": "Get alerts"
|
||||
},
|
||||
"get_forecasts": {
|
||||
"description": "Retrieves the forecast from selected weather services.",
|
||||
"name": "Get forecasts"
|
||||
|
||||
@@ -88,10 +88,6 @@ async def async_migrate_entry(
|
||||
config_entry.minor_version,
|
||||
)
|
||||
|
||||
if config_entry.version > 1 or config_entry.minor_version > 1:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if config_entry.version == 1 and config_entry.minor_version == 1:
|
||||
new_data = {**config_entry.data}
|
||||
new_data[CONF_CONNECTION_TYPE] = HTTP
|
||||
|
||||
@@ -27,6 +27,7 @@ from epson_projector.const import (
|
||||
)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDeviceClass,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
@@ -62,6 +63,7 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_device_class = MediaPlayerDeviceClass.PROJECTOR
|
||||
|
||||
_attr_supported_features = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
|
||||
@@ -166,6 +166,8 @@ class RuntimeEntryData:
|
||||
)
|
||||
loaded_platforms: set[Platform] = field(default_factory=set)
|
||||
platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
||||
# Set once the first connection has finished scanner setup or teardown.
|
||||
first_connect_done: asyncio.Event = field(default_factory=asyncio.Event)
|
||||
_storage_contents: StoreData | None = None
|
||||
_pending_storage: Callable[[], StoreData] | None = None
|
||||
assist_pipeline_update_callbacks: list[CALLBACK_TYPE] = field(default_factory=list)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""Manager for esphome devices."""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
from functools import partial
|
||||
import logging
|
||||
import secrets
|
||||
import struct
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||
from typing import TYPE_CHECKING, Any, Final, NamedTuple
|
||||
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
@@ -106,6 +107,9 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Max time to wait at startup for a BLE proxy to register its scanner.
|
||||
STARTUP_SCANNER_WAIT: Final = 3.0
|
||||
|
||||
LOG_LEVEL_TO_LOGGER = {
|
||||
LogLevel.LOG_LEVEL_NONE: logging.DEBUG,
|
||||
LogLevel.LOG_LEVEL_ERROR: logging.ERROR,
|
||||
@@ -677,6 +681,8 @@ class ESPHomeManager:
|
||||
hass, device_info.bluetooth_mac_address or device_info.mac_address
|
||||
)
|
||||
|
||||
entry_data.first_connect_done.set()
|
||||
|
||||
if device_info.voice_assistant_feature_flags_compat(api_version) and (
|
||||
Platform.ASSIST_SATELLITE not in entry_data.loaded_platforms
|
||||
):
|
||||
@@ -988,6 +994,21 @@ class ESPHomeManager:
|
||||
|
||||
await reconnect_logic.start()
|
||||
|
||||
# Wait for a cached BLE proxy to register its scanner before finishing setup.
|
||||
if (
|
||||
device_info := entry_data.device_info
|
||||
) is not None and device_info.bluetooth_proxy_feature_flags_compat(
|
||||
entry_data.api_version
|
||||
):
|
||||
try:
|
||||
async with asyncio.timeout(STARTUP_SCANNER_WAIT):
|
||||
await entry_data.first_connect_done.wait()
|
||||
except TimeoutError:
|
||||
_LOGGER.debug(
|
||||
"%s: Timed out waiting for Bluetooth scanner to register",
|
||||
self.host,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_setup_device_registry(
|
||||
|
||||
@@ -53,9 +53,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate config entry."""
|
||||
if config_entry.version > 2:
|
||||
# Downgraded from future
|
||||
return False
|
||||
|
||||
if config_entry.version < 2:
|
||||
# Move optional fields from data to options in config entry
|
||||
|
||||
@@ -8,6 +8,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .coordinator import FlussApiClientError, FlussConfigEntry
|
||||
from .entity import FlussEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -15,7 +15,7 @@ from .const import DOMAIN
|
||||
from .coordinator import FlussApiClientError, FlussConfigEntry
|
||||
from .entity import FlussEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
STATUS_OPEN = "Open"
|
||||
STATUS_CLOSED = "Closed"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "fluss",
|
||||
"name": "Fluss+",
|
||||
"codeowners": ["@fluss"],
|
||||
"codeowners": ["@fluss", "@Marcello17"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/fluss",
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user