mirror of
https://github.com/home-assistant/core.git
synced 2026-06-11 11:41:42 +02:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
|
||||
# - 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@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
|
||||
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@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
|
||||
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@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
|
||||
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@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
|
||||
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@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
|
||||
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@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,98 @@
|
||||
"""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 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)
|
||||
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,
|
||||
)
|
||||
@@ -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,55 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_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,6 +3,7 @@
|
||||
from dataclasses import asdict
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from hassil.parse_expression import parse_sentence
|
||||
@@ -204,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)
|
||||
@@ -215,6 +218,11 @@ 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:
|
||||
@@ -222,7 +230,6 @@ def is_valid_sentence(value: list[str]) -> list[str]:
|
||||
parse_sentence(sentence)
|
||||
except ParseError as err:
|
||||
raise vol.Invalid(f"invalid sentence: {err}") from err
|
||||
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"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."]
|
||||
}
|
||||
|
||||
@@ -50,21 +50,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,62 +80,72 @@ 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,
|
||||
),
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
"""The Brands integration."""
|
||||
|
||||
from collections import deque
|
||||
from collections.abc import Container, Mapping
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from random import SystemRandom
|
||||
import time
|
||||
from typing import Any, Final, override
|
||||
from typing import Any, Final
|
||||
|
||||
from aiohttp import ClientError, web
|
||||
from aiohttp import ClientError, hdrs, web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
|
||||
from homeassistant.core import HomeAssistant, callback, valid_domain
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -109,18 +108,23 @@ def _read_brand_file(brand_dir: Path, image: str) -> bytes | None:
|
||||
class _BrandsBaseView(HomeAssistantView):
|
||||
"""Base view for serving brand images."""
|
||||
|
||||
use_query_token_for_auth = True
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the view."""
|
||||
self._hass = hass
|
||||
self._cache_dir = Path(hass.config.cache_path(DOMAIN))
|
||||
|
||||
@callback
|
||||
@override
|
||||
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
|
||||
"""Return valid auth tokens, which can be used for query token authentication."""
|
||||
return self._hass.data[DOMAIN]
|
||||
def _authenticate(self, request: web.Request) -> None:
|
||||
"""Authenticate the request using Bearer token or query token."""
|
||||
access_tokens: deque[str] = self._hass.data[DOMAIN]
|
||||
authenticated = (
|
||||
request[KEY_AUTHENTICATED] or request.query.get("token") in access_tokens
|
||||
)
|
||||
if not authenticated:
|
||||
if hdrs.AUTHORIZATION in request.headers:
|
||||
raise web.HTTPUnauthorized
|
||||
raise web.HTTPForbidden
|
||||
|
||||
async def _serve_from_custom_integration(
|
||||
self,
|
||||
@@ -236,6 +240,8 @@ class BrandsIntegrationView(_BrandsBaseView):
|
||||
image: str,
|
||||
) -> web.Response:
|
||||
"""Handle GET request for an integration brand image."""
|
||||
self._authenticate(request)
|
||||
|
||||
if not valid_domain(domain) or image not in ALLOWED_IMAGES:
|
||||
return web.Response(status=HTTPStatus.NOT_FOUND)
|
||||
|
||||
@@ -268,6 +274,8 @@ class BrandsHardwareView(_BrandsBaseView):
|
||||
image: str,
|
||||
) -> web.Response:
|
||||
"""Handle GET request for a hardware brand image."""
|
||||
self._authenticate(request)
|
||||
|
||||
if not CATEGORY_RE.match(category):
|
||||
return web.Response(status=HTTPStatus.NOT_FOUND)
|
||||
# Hardware images have dynamic names like "manufacturer_model.png"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
from collections.abc import Awaitable, Callable, Container, Coroutine, Mapping
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from contextlib import suppress
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timedelta
|
||||
@@ -12,16 +12,16 @@ import logging
|
||||
import os
|
||||
from random import SystemRandom
|
||||
import time
|
||||
from typing import Any, Final, final, override
|
||||
from typing import Any, Final, final
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp import hdrs, web
|
||||
import attr
|
||||
from propcache.api import cached_property, under_cached_property
|
||||
import voluptuous as vol
|
||||
from webrtc_models import RTCIceCandidateInit
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
@@ -776,26 +776,30 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
class CameraView(HomeAssistantView):
|
||||
"""Base CameraView."""
|
||||
|
||||
use_query_token_for_auth = True
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, component: EntityComponent[Camera]) -> None:
|
||||
"""Initialize a basic camera view."""
|
||||
self.component = component
|
||||
|
||||
@callback
|
||||
@override
|
||||
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
|
||||
"""Return valid auth tokens, which can be used for query token authentication."""
|
||||
if (camera := self.component.get_entity(match_info["entity_id"])) is None:
|
||||
return ()
|
||||
|
||||
return camera.access_tokens
|
||||
|
||||
async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse:
|
||||
"""Start a GET request."""
|
||||
if (camera := self.component.get_entity(entity_id)) is None:
|
||||
raise web.HTTPNotFound
|
||||
|
||||
authenticated = (
|
||||
request[KEY_AUTHENTICATED]
|
||||
or request.query.get("token") in camera.access_tokens
|
||||
)
|
||||
|
||||
if not authenticated:
|
||||
# Attempt with invalid bearer token, raise unauthorized
|
||||
# so ban middleware can handle it.
|
||||
if hdrs.AUTHORIZATION in request.headers:
|
||||
raise web.HTTPUnauthorized
|
||||
# Invalid sigAuth or camera access token
|
||||
raise web.HTTPForbidden
|
||||
|
||||
if not camera.is_on:
|
||||
_LOGGER.debug("Camera is off")
|
||||
raise web.HTTPServiceUnavailable
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
"""Offer sentence based automation rules."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from hassil.parse_expression import parse_sentence
|
||||
@@ -33,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)
|
||||
@@ -44,6 +47,11 @@ 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:
|
||||
@@ -51,7 +59,6 @@ def is_valid_sentence(value: list[str]) -> list[str]:
|
||||
parse_sentence(sentence)
|
||||
except ParseError as err:
|
||||
raise vol.Invalid(f"invalid sentence: {err}") from err
|
||||
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -28,7 +28,7 @@ rules:
|
||||
docs-installation-parameters: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: todo
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
# Gold
|
||||
|
||||
@@ -48,8 +48,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: FGLairConfigEntry) -> b
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: FGLairConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
if entry.version > 1:
|
||||
return False
|
||||
|
||||
if entry.version == 1:
|
||||
new_data = {**entry.data}
|
||||
|
||||
@@ -65,10 +65,6 @@ async def async_migrate_entry(
|
||||
"""Migrate old entry."""
|
||||
_LOGGER.debug("Migrating from version %s", config_entry.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 = {**config_entry.data}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from gardena_bluetooth.const import (
|
||||
AquaContourBattery,
|
||||
@@ -279,7 +279,7 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity):
|
||||
super()._handle_coordinator_update()
|
||||
return
|
||||
|
||||
time = datetime.now(UTC) + timedelta(seconds=value) # pylint: disable=home-assistant-enforce-utcnow
|
||||
time = dt_util.utcnow() + timedelta(seconds=value)
|
||||
if not self._attr_native_value:
|
||||
self._attr_native_value = time
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
|
||||
"description": "Do you want to set up {name}?\n\nBefore you continue, make sure the device is in pairing mode."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
||||
@@ -59,10 +59,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""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 == 1:
|
||||
# Migrate to advanced section
|
||||
new_options = {**entry.options}
|
||||
|
||||
@@ -148,9 +148,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
"Migrating from version %s.%s", config_entry.version, 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:
|
||||
options = {**config_entry.options}
|
||||
if config_entry.minor_version < 2:
|
||||
|
||||
@@ -76,9 +76,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
"Migrating from version %s.%s", config_entry.version, 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:
|
||||
options = {**config_entry.options}
|
||||
if config_entry.minor_version < 2:
|
||||
|
||||
@@ -14,7 +14,12 @@ from homeassistant.helpers.aiohttp_client import (
|
||||
)
|
||||
|
||||
from .const import CONF_REPOSITORIES, CONF_REPOSITORY, DOMAIN, SUBENTRY_TYPE_REPOSITORY
|
||||
from .coordinator import GithubConfigEntry, GitHubDataUpdateCoordinator
|
||||
from .coordinator import (
|
||||
GithubConfigEntry,
|
||||
GitHubDataUpdateCoordinator,
|
||||
GitHubRuntimeData,
|
||||
GitHubUserDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
@@ -27,7 +32,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bo
|
||||
client_name=SERVER_SOFTWARE,
|
||||
)
|
||||
|
||||
entry.runtime_data = {}
|
||||
user_coordinator = GitHubUserDataUpdateCoordinator(
|
||||
hass=hass,
|
||||
config_entry=entry,
|
||||
client=client,
|
||||
)
|
||||
await user_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
repositories: dict[str, GitHubDataUpdateCoordinator] = {}
|
||||
for repository_subentry in entry.get_subentries_of_type(SUBENTRY_TYPE_REPOSITORY):
|
||||
repository = repository_subentry.data[CONF_REPOSITORY]
|
||||
coordinator = GitHubDataUpdateCoordinator(
|
||||
@@ -42,7 +54,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bo
|
||||
if not entry.pref_disable_polling:
|
||||
await coordinator.subscribe()
|
||||
|
||||
entry.runtime_data[repository_subentry.subentry_id] = coordinator
|
||||
repositories[repository_subentry.subentry_id] = coordinator
|
||||
|
||||
entry.runtime_data = GitHubRuntimeData(
|
||||
user_coordinator=user_coordinator,
|
||||
repositories=repositories,
|
||||
)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_entry))
|
||||
|
||||
@@ -57,8 +74,7 @@ async def async_update_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> N
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
repositories = entry.runtime_data
|
||||
for coordinator in repositories.values():
|
||||
for coordinator in entry.runtime_data.repositories.values():
|
||||
coordinator.unsubscribe()
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Custom data update coordinator for the GitHub integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from aiogithubapi import (
|
||||
GitHubAPI,
|
||||
GitHubAuthenticatedUserModel,
|
||||
GitHubConnectionException,
|
||||
GitHubEventModel,
|
||||
GitHubException,
|
||||
@@ -103,7 +105,52 @@ query ($owner: String!, $repository: String!) {
|
||||
}
|
||||
"""
|
||||
|
||||
type GithubConfigEntry = ConfigEntry[dict[str, GitHubDataUpdateCoordinator]]
|
||||
type GithubConfigEntry = ConfigEntry[GitHubRuntimeData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class GitHubRuntimeData:
|
||||
"""Runtime data for the GitHub integration."""
|
||||
|
||||
user_coordinator: GitHubUserDataUpdateCoordinator
|
||||
repositories: dict[str, GitHubDataUpdateCoordinator]
|
||||
|
||||
|
||||
class GitHubUserDataUpdateCoordinator(
|
||||
DataUpdateCoordinator[GitHubAuthenticatedUserModel]
|
||||
):
|
||||
"""Data update coordinator for the authenticated GitHub user."""
|
||||
|
||||
config_entry: GithubConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: GithubConfigEntry,
|
||||
client: GitHubAPI,
|
||||
) -> None:
|
||||
"""Initialize GitHub user data update coordinator."""
|
||||
self._client = client
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
config_entry=config_entry,
|
||||
name="user",
|
||||
update_interval=FALLBACK_UPDATE_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> GitHubAuthenticatedUserModel:
|
||||
"""Update data."""
|
||||
try:
|
||||
response = await self._client.user.get()
|
||||
except (GitHubConnectionException, GitHubRatelimitException) as exception:
|
||||
raise UpdateFailed(exception) from exception
|
||||
except GitHubException as exception:
|
||||
LOGGER.exception(exception)
|
||||
raise UpdateFailed(exception) from exception
|
||||
|
||||
return response.data
|
||||
|
||||
|
||||
class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
@@ -33,7 +33,7 @@ async def async_get_config_entry_diagnostics(
|
||||
else:
|
||||
data["rate_limit"] = rate_limit_response.data.as_dict
|
||||
|
||||
repositories = config_entry.runtime_data
|
||||
repositories = config_entry.runtime_data.repositories
|
||||
data["repositories"] = {}
|
||||
|
||||
for coordinator in repositories.values():
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
"discussions_count": {
|
||||
"default": "mdi:forum"
|
||||
},
|
||||
"followers": {
|
||||
"default": "mdi:account-multiple"
|
||||
},
|
||||
"following": {
|
||||
"default": "mdi:account-multiple-outline"
|
||||
},
|
||||
"forks_count": {
|
||||
"default": "mdi:source-fork"
|
||||
},
|
||||
@@ -31,6 +37,12 @@
|
||||
"merged_pulls_count": {
|
||||
"default": "mdi:source-merge"
|
||||
},
|
||||
"public_gists": {
|
||||
"default": "mdi:code-json"
|
||||
},
|
||||
"public_repos": {
|
||||
"default": "mdi:source-repository"
|
||||
},
|
||||
"pulls_count": {
|
||||
"default": "mdi:source-pull"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,8 @@ from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from aiogithubapi import GitHubAuthenticatedUserModel
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
@@ -17,7 +19,11 @@ from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GithubConfigEntry, GitHubDataUpdateCoordinator
|
||||
from .coordinator import (
|
||||
GithubConfigEntry,
|
||||
GitHubDataUpdateCoordinator,
|
||||
GitHubUserDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -141,14 +147,58 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = (
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GitHubUserSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes GitHub user sensor entity."""
|
||||
|
||||
value_fn: Callable[[GitHubAuthenticatedUserModel], StateType]
|
||||
|
||||
|
||||
USER_SENSOR_DESCRIPTIONS: tuple[GitHubUserSensorEntityDescription, ...] = (
|
||||
GitHubUserSensorEntityDescription(
|
||||
key="followers",
|
||||
translation_key="followers",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: data.followers,
|
||||
),
|
||||
GitHubUserSensorEntityDescription(
|
||||
key="following",
|
||||
translation_key="following",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: data.following,
|
||||
),
|
||||
GitHubUserSensorEntityDescription(
|
||||
key="public_gists",
|
||||
translation_key="public_gists",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: data.public_gists,
|
||||
),
|
||||
GitHubUserSensorEntityDescription(
|
||||
key="public_repos",
|
||||
translation_key="public_repos",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: data.public_repos,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: GithubConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up GitHub sensor based on a config entry."""
|
||||
repositories = entry.runtime_data
|
||||
for subentry_id, coordinator in repositories.items():
|
||||
user_coordinator = entry.runtime_data.user_coordinator
|
||||
async_add_entities(
|
||||
GitHubUserSensorEntity(user_coordinator, description)
|
||||
for description in USER_SENSOR_DESCRIPTIONS
|
||||
)
|
||||
|
||||
for subentry_id, coordinator in entry.runtime_data.repositories.items():
|
||||
async_add_entities(
|
||||
(
|
||||
GitHubSensorEntity(coordinator, description)
|
||||
@@ -203,3 +253,37 @@ class GitHubSensorEntity(CoordinatorEntity[GitHubDataUpdateCoordinator], SensorE
|
||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
"""Return the extra state attributes."""
|
||||
return self.entity_description.attr_fn(self.coordinator.data)
|
||||
|
||||
|
||||
class GitHubUserSensorEntity(
|
||||
CoordinatorEntity[GitHubUserDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Defines a GitHub user sensor entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
entity_description: GitHubUserSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: GitHubUserDataUpdateCoordinator,
|
||||
entity_description: GitHubUserSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{coordinator.data.id}_{entity_description.key}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, str(coordinator.data.id))},
|
||||
name=coordinator.data.login,
|
||||
manufacturer="GitHub",
|
||||
configuration_url=f"https://github.com/{coordinator.data.login}",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
"name": "Discussions",
|
||||
"unit_of_measurement": "discussions"
|
||||
},
|
||||
"followers": {
|
||||
"name": "Followers",
|
||||
"unit_of_measurement": "followers"
|
||||
},
|
||||
"following": {
|
||||
"name": "Following",
|
||||
"unit_of_measurement": "users"
|
||||
},
|
||||
"forks_count": {
|
||||
"name": "Forks",
|
||||
"unit_of_measurement": "forks"
|
||||
@@ -66,6 +74,14 @@
|
||||
"name": "Merged pull requests",
|
||||
"unit_of_measurement": "pull requests"
|
||||
},
|
||||
"public_gists": {
|
||||
"name": "Public gists",
|
||||
"unit_of_measurement": "gists"
|
||||
},
|
||||
"public_repos": {
|
||||
"name": "Public repositories",
|
||||
"unit_of_measurement": "repositories"
|
||||
},
|
||||
"pulls_count": {
|
||||
"name": "Pull requests",
|
||||
"unit_of_measurement": "pull requests"
|
||||
|
||||
@@ -98,10 +98,6 @@ async def async_migrate_entry(
|
||||
) -> bool:
|
||||
"""Migrate old config entries."""
|
||||
|
||||
if config_entry.version > 2:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if config_entry.version == 1:
|
||||
# Update from version 1 to version 2 adding the PROTOCOL to the config entry
|
||||
host = config_entry.data[CONF_HOST]
|
||||
|
||||
@@ -226,10 +226,6 @@ async def async_migrate_entry(
|
||||
"""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:
|
||||
# Add TTS subentry which was missing in 2025.7.0b0
|
||||
if not any(
|
||||
|
||||
@@ -100,9 +100,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
"Migrating from version %s.%s", config_entry.version, 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:
|
||||
options = {**config_entry.options}
|
||||
if config_entry.minor_version < 2:
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.98", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.98", "babel==2.18.0"]
|
||||
}
|
||||
|
||||
@@ -108,10 +108,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:
|
||||
if config_entry.minor_version == 1:
|
||||
serial_number = config_entry.data[SERIAL_NUMBER]
|
||||
@@ -135,7 +131,24 @@ async def async_migrate_entry(
|
||||
)
|
||||
|
||||
if canonical.entry_id != config_entry.entry_id:
|
||||
# The canonical entry's migration will remove this duplicate.
|
||||
if canonical.minor_version < 2:
|
||||
# The canonical entry has not been migrated yet and its
|
||||
# migration will remove this duplicate.
|
||||
return False
|
||||
|
||||
# The canonical entry is already fully migrated and will not run
|
||||
# a migration that removes this duplicate, so remove it here. The
|
||||
# entry can't remove itself while its setup lock is held, so
|
||||
# schedule the removal instead.
|
||||
_LOGGER.debug(
|
||||
"Removing duplicate config entry %s for serial %s in favor of %s",
|
||||
config_entry.entry_id,
|
||||
serial_number,
|
||||
canonical.entry_id,
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_remove(config_entry.entry_id)
|
||||
)
|
||||
return False
|
||||
|
||||
for duplicate in duplicates:
|
||||
|
||||
@@ -140,10 +140,6 @@ async def async_migrate_entry(
|
||||
"Migrating from version %s.%s", config_entry.version, 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 == 1:
|
||||
# Add-on startup with type service get started before
|
||||
@@ -239,7 +235,24 @@ async def async_migrate_entry(
|
||||
)
|
||||
|
||||
if canonical.entry_id != config_entry.entry_id:
|
||||
# The canonical entry's migration will remove this duplicate.
|
||||
if canonical.minor_version < 5:
|
||||
# The canonical entry has not been migrated yet and its
|
||||
# migration will remove this duplicate.
|
||||
return False
|
||||
|
||||
# The canonical entry is already fully migrated and will not run
|
||||
# a migration that removes this duplicate, so remove it here. The
|
||||
# entry can't remove itself while its setup lock is held, so
|
||||
# schedule the removal instead.
|
||||
_LOGGER.warning(
|
||||
"Removing duplicate config entry %s for serial %s in favor of %s",
|
||||
config_entry.entry_id,
|
||||
serial_number,
|
||||
canonical.entry_id,
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_remove(config_entry.entry_id)
|
||||
)
|
||||
return False
|
||||
|
||||
for duplicate in duplicates:
|
||||
|
||||
@@ -46,7 +46,7 @@ class DeviceTriggerAccessory(HomeAccessory):
|
||||
ent_reg = er.async_get(self.hass)
|
||||
for idx, trigger in enumerate(device_triggers):
|
||||
type_: str = trigger["type"]
|
||||
subtype: str | None = trigger.get("subtype")
|
||||
subtype: str | int | None = trigger.get("subtype")
|
||||
unique_id = f"{type_}-{subtype or ''}"
|
||||
entity_id: str | None = None
|
||||
if (entity_id_or_uuid := trigger.get("entity_id")) and (
|
||||
@@ -61,7 +61,7 @@ class DeviceTriggerAccessory(HomeAccessory):
|
||||
trigger_name_parts.append(state.name)
|
||||
trigger_name_parts.append(type_.replace("_", " ").title())
|
||||
if subtype:
|
||||
trigger_name_parts.append(subtype.replace("_", " ").title())
|
||||
trigger_name_parts.append(str(subtype).replace("_", " ").title())
|
||||
trigger_name = cleanup_name_for_homekit(" ".join(trigger_name_parts))
|
||||
serv_stateless_switch = self.add_preload_service(
|
||||
SERV_STATELESS_PROGRAMMABLE_SWITCH,
|
||||
|
||||
@@ -126,8 +126,6 @@ async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: config_entries.ConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate the config entry from version 1 to version 2."""
|
||||
if config_entry.version > 2:
|
||||
return False
|
||||
|
||||
if config_entry.version == 1:
|
||||
_LOGGER.debug("Migrating HomematicIP Cloud config entry to version 2")
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.12.0"]
|
||||
"requirements": ["homematicip==2.13.0"]
|
||||
}
|
||||
|
||||
@@ -79,9 +79,8 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry]
|
||||
data = await self.api.combined()
|
||||
|
||||
except RequestError as ex:
|
||||
# pylint: disable-next=home-assistant-exception-message-with-translation
|
||||
raise UpdateFailed(
|
||||
ex, translation_domain=DOMAIN, translation_key="communication_error"
|
||||
translation_domain=DOMAIN, translation_key="communication_error"
|
||||
) from ex
|
||||
|
||||
except DisabledError as ex:
|
||||
@@ -96,9 +95,8 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry]
|
||||
self.config_entry.entry_id
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-exception-message-with-translation
|
||||
raise UpdateFailed(
|
||||
ex, translation_domain=DOMAIN, translation_key="api_disabled"
|
||||
translation_domain=DOMAIN, translation_key="api_disabled"
|
||||
) from ex
|
||||
|
||||
except UnauthorizedError as ex:
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
"""The coordinator for Hypontech Cloud integration."""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
from hyponcloud import HyponCloud, OverviewData, PlantData, RequestError
|
||||
from hyponcloud import (
|
||||
HyponCloud,
|
||||
OverviewData,
|
||||
PlantData,
|
||||
PlantMonitorData,
|
||||
RequestError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -12,12 +19,20 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
|
||||
@dataclass
|
||||
class HypontechPlant:
|
||||
"""Store a plant together with its real-time monitor data."""
|
||||
|
||||
info: PlantData
|
||||
monitor: PlantMonitorData
|
||||
|
||||
|
||||
@dataclass
|
||||
class HypontechCoordinatorData:
|
||||
"""Store coordinator data."""
|
||||
|
||||
overview: OverviewData
|
||||
plants: dict[str, PlantData]
|
||||
plants: dict[str, HypontechPlant]
|
||||
|
||||
|
||||
type HypontechConfigEntry = ConfigEntry[HypontechDataCoordinator]
|
||||
@@ -50,11 +65,17 @@ class HypontechDataCoordinator(DataUpdateCoordinator[HypontechCoordinatorData]):
|
||||
try:
|
||||
overview = await self.api.get_overview()
|
||||
plants = await self.api.get_list()
|
||||
monitors = await asyncio.gather(
|
||||
*(self.api.get_monitor(plant.plant_id) for plant in plants)
|
||||
)
|
||||
except RequestError as ex:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN, translation_key="connection_error"
|
||||
) from ex
|
||||
return HypontechCoordinatorData(
|
||||
overview=overview,
|
||||
plants={plant.plant_id: plant for plant in plants},
|
||||
plants={
|
||||
plant.plant_id: HypontechPlant(info=plant, monitor=monitor)
|
||||
for plant, monitor in zip(plants, monitors, strict=True)
|
||||
},
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user