This commit is contained in:
Franck Nijhof 2024-11-06 20:10:51 +01:00 committed by GitHub
commit 18e12740d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2634 changed files with 118031 additions and 39775 deletions

View File

@ -127,6 +127,7 @@ tests: &tests
- tests/*.py - tests/*.py
- tests/auth/** - tests/auth/**
- tests/backports/** - tests/backports/**
- tests/components/conftest.py
- tests/components/diagnostics/** - tests/components/diagnostics/**
- tests/components/history/** - tests/components/history/**
- tests/components/logbook/** - tests/components/logbook/**

View File

@ -2,7 +2,7 @@
"name": "Home Assistant Dev", "name": "Home Assistant Dev",
"context": "..", "context": "..",
"dockerFile": "../Dockerfile.dev", "dockerFile": "../Dockerfile.dev",
"postCreateCommand": "script/setup", "postCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && script/setup",
"postStartCommand": "script/bootstrap", "postStartCommand": "script/bootstrap",
"containerEnv": { "containerEnv": {
"PYTHONASYNCIODEBUG": "1" "PYTHONASYNCIODEBUG": "1"
@ -12,7 +12,12 @@
}, },
// Port 5683 udp is used by Shelly integration // Port 5683 udp is used by Shelly integration
"appPort": ["8123:8123", "5683:5683/udp"], "appPort": ["8123:8123", "5683:5683/udp"],
"runArgs": ["-e", "GIT_EDITOR=code --wait"], "runArgs": [
"-e",
"GIT_EDITOR=code --wait",
"--security-opt",
"label=disable"
],
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [
@ -53,7 +58,13 @@
], ],
"[python]": { "[python]": {
"editor.defaultFormatter": "charliermarsh.ruff" "editor.defaultFormatter": "charliermarsh.ruff"
} },
"json.schemas": [
{
"fileMatch": ["homeassistant/components/*/manifest.json"],
"url": "./script/json_schemas/manifest_schema.json"
}
]
} }
} }
} }

View File

@ -7,6 +7,7 @@ docs
# Development # Development
.devcontainer .devcontainer
.vscode .vscode
.tool-versions
# Test related files # Test related files
tests tests

3
.github/FUNDING.yml vendored
View File

@ -1,2 +1 @@
custom: https://www.nabucasa.com custom: https://www.openhomefoundation.org
github: balloob

View File

@ -27,12 +27,12 @@ jobs:
publish: ${{ steps.version.outputs.publish }} publish: ${{ steps.version.outputs.publish }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -69,7 +69,7 @@ jobs:
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations - name: Upload translations
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: translations name: translations
path: translations.tar.gz path: translations.tar.gz
@ -90,7 +90,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Download nightly wheels of frontend - name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
@ -116,7 +116,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -242,7 +242,7 @@ jobs:
- green - green
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set build additional args - name: Set build additional args
run: | run: |
@ -279,7 +279,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Initialize git - name: Initialize git
uses: home-assistant/actions/helpers/git-init@master uses: home-assistant/actions/helpers/git-init@master
@ -321,10 +321,10 @@ jobs:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"] registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@v3.6.0 uses: sigstore/cosign-installer@v3.7.0
with: with:
cosign-release: "v2.2.3" cosign-release: "v2.2.3"
@ -451,10 +451,10 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -499,7 +499,7 @@ jobs:
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }} HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image - name: Build Docker image
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
with: with:
context: . # So action will not pull the repository again context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile file: ./script/hassfest/docker/Dockerfile
@ -522,7 +522,7 @@ jobs:
- name: Push Docker image - name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push id: push
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
with: with:
context: . # So action will not pull the repository again context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile file: ./script/hassfest/docker/Dockerfile

View File

@ -40,7 +40,7 @@ env:
CACHE_VERSION: 11 CACHE_VERSION: 11
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9 MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2024.10" HA_SHORT_VERSION: "2024.11"
DEFAULT_PYTHON: "3.12" DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12']" ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
@ -93,7 +93,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Generate partial Python venv restore key - name: Generate partial Python venv restore key
id: generate_python_cache_key id: generate_python_cache_key
run: | run: |
@ -231,16 +231,16 @@ jobs:
- info - info
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.0.2 uses: actions/cache@v4.1.2
with: with:
path: venv path: venv
key: >- key: >-
@ -256,7 +256,7 @@ jobs:
uv pip install "$(cat requirements_test.txt | grep pre-commit)" uv pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v4.0.2 uses: actions/cache@v4.1.2
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true lookup-only: true
@ -277,16 +277,16 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -295,7 +295,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true fail-on-cache-miss: true
@ -317,16 +317,16 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -335,7 +335,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true fail-on-cache-miss: true
@ -357,16 +357,16 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -375,7 +375,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true fail-on-cache-miss: true
@ -447,7 +447,7 @@ jobs:
- script/hassfest/docker/Dockerfile - script/hassfest/docker/Dockerfile
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Register hadolint problem matcher - name: Register hadolint problem matcher
run: | run: |
echo "::add-matcher::.github/workflows/matchers/hadolint.json" echo "::add-matcher::.github/workflows/matchers/hadolint.json"
@ -466,10 +466,10 @@ jobs:
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -482,7 +482,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.0.2 uses: actions/cache@v4.1.2
with: with:
path: venv path: venv
lookup-only: true lookup-only: true
@ -491,7 +491,7 @@ jobs:
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache - name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@v4.0.2 uses: actions/cache@v4.1.2
with: with:
path: ${{ env.UV_CACHE_DIR }} path: ${{ env.UV_CACHE_DIR }}
key: >- key: >-
@ -550,16 +550,16 @@ jobs:
sudo apt-get -y install \ sudo apt-get -y install \
libturbojpeg libturbojpeg
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -583,16 +583,16 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -615,37 +615,41 @@ jobs:
&& github.event.inputs.mypy-only != 'true' && github.event.inputs.mypy-only != 'true'
|| github.event.inputs.audit-licenses-only == 'true') || github.event.inputs.audit-licenses-only == 'true')
&& needs.info.outputs.requirements == 'true' && needs.info.outputs.requirements == 'true'
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Run pip-licenses - name: Extract license data
run: | run: |
. venv/bin/activate . venv/bin/activate
pip-licenses --format=json --output-file=licenses.json python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json
- name: Upload licenses - name: Upload licenses
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: licenses name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
path: licenses.json path: licenses-${{ matrix.python-version }}.json
- name: Process licenses - name: Check licenses
run: | run: |
. venv/bin/activate . venv/bin/activate
python -m script.licenses python -m script.licenses check licenses-${{ matrix.python-version }}.json
pylint: pylint:
name: Check pylint name: Check pylint
@ -660,16 +664,16 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -707,16 +711,16 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -752,10 +756,10 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -768,7 +772,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -776,7 +780,7 @@ jobs:
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Restore mypy cache - name: Restore mypy cache
uses: actions/cache@v4.0.2 uses: actions/cache@v4.1.2
with: with:
path: .mypy_cache path: .mypy_cache
key: >- key: >-
@ -815,7 +819,11 @@ jobs:
needs: needs:
- info - info
- base - base
name: Split tests for full run strategy:
fail-fast: false
matrix:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
name: Split tests for full run Python ${{ matrix.python-version }}
steps: steps:
- name: Install additional OS dependencies - name: Install additional OS dependencies
run: | run: |
@ -827,16 +835,16 @@ jobs:
libturbojpeg \ libturbojpeg \
libgammu-dev libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -848,9 +856,9 @@ jobs:
. venv/bin/activate . venv/bin/activate
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
- name: Upload pytest_buckets - name: Upload pytest_buckets
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: pytest_buckets name: pytest_buckets-${{ matrix.python-version }}
path: pytest_buckets.txt path: pytest_buckets.txt
overwrite: true overwrite: true
@ -891,16 +899,16 @@ jobs:
libturbojpeg \ libturbojpeg \
libgammu-dev libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -915,7 +923,7 @@ jobs:
- name: Download pytest_buckets - name: Download pytest_buckets
uses: actions/download-artifact@v4.1.8 uses: actions/download-artifact@v4.1.8
with: with:
name: pytest_buckets name: pytest_buckets-${{ matrix.python-version }}
- name: Compile English translations - name: Compile English translations
run: | run: |
. venv/bin/activate . venv/bin/activate
@ -940,7 +948,7 @@ jobs:
-qq \ -qq \
--timeout=9 \ --timeout=9 \
--durations=10 \ --durations=10 \
-n auto \ --numprocesses auto \
--dist=loadfile \ --dist=loadfile \
${cov_params[@]} \ ${cov_params[@]} \
-o console_output_style=count \ -o console_output_style=count \
@ -949,14 +957,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-full.conclusion == 'failure' if: success() || failure() && steps.pytest-full.conclusion == 'failure'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt path: pytest-*.txt
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }} name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml path: coverage.xml
@ -1011,16 +1019,16 @@ jobs:
libturbojpeg \ libturbojpeg \
libmariadb-dev-compat libmariadb-dev-compat
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -1062,7 +1070,7 @@ jobs:
python3 -b -X dev -m pytest \ python3 -b -X dev -m pytest \
-qq \ -qq \
--timeout=20 \ --timeout=20 \
-n 1 \ --numprocesses 1 \
${cov_params[@]} \ ${cov_params[@]} \
-o console_output_style=count \ -o console_output_style=count \
--durations=10 \ --durations=10 \
@ -1075,7 +1083,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }} steps.pytest-partial.outputs.mariadb }}
@ -1083,7 +1091,7 @@ jobs:
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: coverage-${{ matrix.python-version }}-${{ name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }} steps.pytest-partial.outputs.mariadb }}
@ -1094,7 +1102,7 @@ jobs:
./script/check_dirty ./script/check_dirty
pytest-postgres: pytest-postgres:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
services: services:
postgres: postgres:
image: ${{ matrix.postgresql-group }} image: ${{ matrix.postgresql-group }}
@ -1134,19 +1142,21 @@ jobs:
sudo apt-get -y install \ sudo apt-get -y install \
bluez \ bluez \
ffmpeg \ ffmpeg \
libturbojpeg \ libturbojpeg
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
sudo apt-get -y install \
postgresql-server-dev-14 postgresql-server-dev-14
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -1188,7 +1198,7 @@ jobs:
python3 -b -X dev -m pytest \ python3 -b -X dev -m pytest \
-qq \ -qq \
--timeout=9 \ --timeout=9 \
-n 1 \ --numprocesses 1 \
${cov_params[@]} \ ${cov_params[@]} \
-o console_output_style=count \ -o console_output_style=count \
--durations=0 \ --durations=0 \
@ -1202,7 +1212,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }} steps.pytest-partial.outputs.postgresql }}
@ -1210,7 +1220,7 @@ jobs:
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: coverage-${{ matrix.python-version }}-${{ name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }} steps.pytest-partial.outputs.postgresql }}
@ -1232,14 +1242,14 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.8 uses: actions/download-artifact@v4.1.8
with: with:
pattern: coverage-* pattern: coverage-*
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true' if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v4.5.0 uses: codecov/codecov-action@v4.6.0
with: with:
fail_ci_if_error: true fail_ci_if_error: true
flags: full-suite flags: full-suite
@ -1283,16 +1293,16 @@ jobs:
libturbojpeg \ libturbojpeg \
libgammu-dev libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.1.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -1334,7 +1344,7 @@ jobs:
python3 -b -X dev -m pytest \ python3 -b -X dev -m pytest \
-qq \ -qq \
--timeout=9 \ --timeout=9 \
-n auto \ --numprocesses auto \
${cov_params[@]} \ ${cov_params[@]} \
-o console_output_style=count \ -o console_output_style=count \
--durations=0 \ --durations=0 \
@ -1344,14 +1354,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt path: pytest-*.txt
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }} name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml path: coverage.xml
@ -1370,14 +1380,14 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.8 uses: actions/download-artifact@v4.1.8
with: with:
pattern: coverage-* pattern: coverage-*
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false' if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v4.5.0 uses: codecov/codecov-action@v4.6.0
with: with:
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -21,14 +21,14 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3.26.9 uses: github/codeql-action/init@v3.27.0
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.26.9 uses: github/codeql-action/analyze@v3.27.0
with: with:
category: "/language:python" category: "/language:python"

View File

@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}

View File

@ -32,11 +32,11 @@ jobs:
architectures: ${{ steps.info.outputs.architectures }} architectures: ${{ steps.info.outputs.architectures }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.2.0 uses: actions/setup-python@v5.3.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -64,11 +64,8 @@ jobs:
- name: Write env-file - name: Write env-file
run: | run: |
( (
echo "GRPC_BUILD_WITH_BORING_SSL_ASM=false"
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true" echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true"
echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true" echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true"
echo "GRPC_PYTHON_DISABLE_LIBC_COMPATIBILITY=true"
echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc"
# Fix out of memory issues with rust # Fix out of memory issues with rust
echo "CARGO_NET_GIT_FETCH_WITH_CLI=true" echo "CARGO_NET_GIT_FETCH_WITH_CLI=true"
@ -82,7 +79,7 @@ jobs:
) > .env_file ) > .env_file
- name: Upload env_file - name: Upload env_file
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: env_file name: env_file
path: ./.env_file path: ./.env_file
@ -90,7 +87,7 @@ jobs:
overwrite: true overwrite: true
- name: Upload requirements_diff - name: Upload requirements_diff
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: requirements_diff name: requirements_diff
path: ./requirements_diff.txt path: ./requirements_diff.txt
@ -102,7 +99,7 @@ jobs:
python -m script.gen_requirements_all ci python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels - name: Upload requirements_all_wheels
uses: actions/upload-artifact@v4.4.0 uses: actions/upload-artifact@v4.4.3
with: with:
name: requirements_all_wheels name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt path: ./requirements_all_wheels_*.txt
@ -119,7 +116,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.8 uses: actions/download-artifact@v4.1.8
@ -163,7 +160,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.8 uses: actions/download-artifact@v4.1.8
@ -205,11 +202,9 @@ jobs:
# Some dependencies still require 'cython<3' # Some dependencies still require 'cython<3'
# and don't yet use isolated build environments. # and don't yet use isolated build environments.
# Build these first. # Build these first.
# grpcio: https://github.com/grpc/grpc/issues/33918
# pydantic: https://github.com/pydantic/pydantic/issues/7689 # pydantic: https://github.com/pydantic/pydantic/issues/7689
touch requirements_old-cython.txt touch requirements_old-cython.txt
cat homeassistant/package_constraints.txt | grep 'grpcio==' >> requirements_old-cython.txt
cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt
- name: Build wheels (old cython) - name: Build wheels (old cython)
@ -221,7 +216,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_old-cython.txt" requirements: "requirements_old-cython.txt"
@ -236,7 +231,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa" requirements: "requirements_all.txtaa"
@ -250,7 +245,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab" requirements: "requirements_all.txtab"
@ -264,7 +259,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac" requirements: "requirements_all.txtac"

1
.gitignore vendored
View File

@ -79,6 +79,7 @@ pytest-*.txt
.pydevproject .pydevproject
.python-version .python-version
.tool-versions
# emacs auto backups # emacs auto backups
*~ *~

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.6 rev: v0.7.1
hooks: hooks:
- id: ruff - id: ruff
args: args:
@ -83,10 +83,10 @@ repos:
pass_filenames: false pass_filenames: false
language: script language: script
types: [text] types: [text]
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements\.txt)$ files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
- id: hassfest-metadata - id: hassfest-metadata
name: hassfest-metadata name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata entry: script/run-in-env.sh python3 -m script.hassfest -p metadata,docker
pass_filenames: false pass_filenames: false
language: script language: script
types: [text] types: [text]

View File

@ -124,6 +124,7 @@ homeassistant.components.bryant_evolution.*
homeassistant.components.bthome.* homeassistant.components.bthome.*
homeassistant.components.button.* homeassistant.components.button.*
homeassistant.components.calendar.* homeassistant.components.calendar.*
homeassistant.components.cambridge_audio.*
homeassistant.components.camera.* homeassistant.components.camera.*
homeassistant.components.canary.* homeassistant.components.canary.*
homeassistant.components.cert_expiry.* homeassistant.components.cert_expiry.*
@ -208,12 +209,14 @@ homeassistant.components.geo_location.*
homeassistant.components.geocaching.* homeassistant.components.geocaching.*
homeassistant.components.gios.* homeassistant.components.gios.*
homeassistant.components.glances.* homeassistant.components.glances.*
homeassistant.components.go2rtc.*
homeassistant.components.goalzero.* homeassistant.components.goalzero.*
homeassistant.components.google.* homeassistant.components.google.*
homeassistant.components.google_assistant_sdk.* homeassistant.components.google_assistant_sdk.*
homeassistant.components.google_cloud.* homeassistant.components.google_cloud.*
homeassistant.components.google_photos.* homeassistant.components.google_photos.*
homeassistant.components.google_sheets.* homeassistant.components.google_sheets.*
homeassistant.components.govee_ble.*
homeassistant.components.gpsd.* homeassistant.components.gpsd.*
homeassistant.components.greeneye_monitor.* homeassistant.components.greeneye_monitor.*
homeassistant.components.group.* homeassistant.components.group.*
@ -301,7 +304,6 @@ homeassistant.components.lookin.*
homeassistant.components.luftdaten.* homeassistant.components.luftdaten.*
homeassistant.components.madvr.* homeassistant.components.madvr.*
homeassistant.components.manual.* homeassistant.components.manual.*
homeassistant.components.map.*
homeassistant.components.mastodon.* homeassistant.components.mastodon.*
homeassistant.components.matrix.* homeassistant.components.matrix.*
homeassistant.components.matter.* homeassistant.components.matter.*
@ -345,6 +347,7 @@ homeassistant.components.oncue.*
homeassistant.components.onewire.* homeassistant.components.onewire.*
homeassistant.components.onkyo.* homeassistant.components.onkyo.*
homeassistant.components.open_meteo.* homeassistant.components.open_meteo.*
homeassistant.components.openai_conversation.*
homeassistant.components.openexchangerates.* homeassistant.components.openexchangerates.*
homeassistant.components.opensky.* homeassistant.components.opensky.*
homeassistant.components.openuv.* homeassistant.components.openuv.*
@ -352,6 +355,7 @@ homeassistant.components.oralb.*
homeassistant.components.otbr.* homeassistant.components.otbr.*
homeassistant.components.overkiz.* homeassistant.components.overkiz.*
homeassistant.components.p1_monitor.* homeassistant.components.p1_monitor.*
homeassistant.components.panel_custom.*
homeassistant.components.peco.* homeassistant.components.peco.*
homeassistant.components.persistent_notification.* homeassistant.components.persistent_notification.*
homeassistant.components.pi_hole.* homeassistant.components.pi_hole.*
@ -369,6 +373,7 @@ homeassistant.components.pvoutput.*
homeassistant.components.qnap_qsw.* homeassistant.components.qnap_qsw.*
homeassistant.components.rabbitair.* homeassistant.components.rabbitair.*
homeassistant.components.radarr.* homeassistant.components.radarr.*
homeassistant.components.radio_browser.*
homeassistant.components.rainforest_raven.* homeassistant.components.rainforest_raven.*
homeassistant.components.rainmachine.* homeassistant.components.rainmachine.*
homeassistant.components.raspberry_pi.* homeassistant.components.raspberry_pi.*
@ -406,6 +411,7 @@ homeassistant.components.sensor.*
homeassistant.components.sensoterra.* homeassistant.components.sensoterra.*
homeassistant.components.senz.* homeassistant.components.senz.*
homeassistant.components.sfr_box.* homeassistant.components.sfr_box.*
homeassistant.components.shell_command.*
homeassistant.components.shelly.* homeassistant.components.shelly.*
homeassistant.components.shopping_list.* homeassistant.components.shopping_list.*
homeassistant.components.simplepush.* homeassistant.components.simplepush.*
@ -420,6 +426,7 @@ homeassistant.components.snooz.*
homeassistant.components.solarlog.* homeassistant.components.solarlog.*
homeassistant.components.sonarr.* homeassistant.components.sonarr.*
homeassistant.components.speedtestdotnet.* homeassistant.components.speedtestdotnet.*
homeassistant.components.spotify.*
homeassistant.components.sql.* homeassistant.components.sql.*
homeassistant.components.squeezebox.* homeassistant.components.squeezebox.*
homeassistant.components.ssdp.* homeassistant.components.ssdp.*
@ -434,6 +441,7 @@ homeassistant.components.suez_water.*
homeassistant.components.sun.* homeassistant.components.sun.*
homeassistant.components.surepetcare.* homeassistant.components.surepetcare.*
homeassistant.components.switch.* homeassistant.components.switch.*
homeassistant.components.switch_as_x.*
homeassistant.components.switchbee.* homeassistant.components.switchbee.*
homeassistant.components.switchbot_cloud.* homeassistant.components.switchbot_cloud.*
homeassistant.components.switcher_kis.* homeassistant.components.switcher_kis.*
@ -502,6 +510,7 @@ homeassistant.components.whois.*
homeassistant.components.withings.* homeassistant.components.withings.*
homeassistant.components.wiz.* homeassistant.components.wiz.*
homeassistant.components.wled.* homeassistant.components.wled.*
homeassistant.components.workday.*
homeassistant.components.worldclock.* homeassistant.components.worldclock.*
homeassistant.components.xiaomi_ble.* homeassistant.components.xiaomi_ble.*
homeassistant.components.yale_smart_alarm.* homeassistant.components.yale_smart_alarm.*

View File

@ -6,5 +6,13 @@
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings // https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
"python.testing.pytestEnabled": false, "python.testing.pytestEnabled": false,
// https://code.visualstudio.com/docs/python/linting#_general-settings // https://code.visualstudio.com/docs/python/linting#_general-settings
"pylint.importStrategy": "fromEnvironment" "pylint.importStrategy": "fromEnvironment",
"json.schemas": [
{
"fileMatch": [
"homeassistant/components/*/manifest.json"
],
"url": "./script/json_schemas/manifest_schema.json"
}
]
} }

View File

@ -544,6 +544,8 @@ build.json @home-assistant/supervisor
/tests/components/github/ @timmo001 @ludeeus /tests/components/github/ @timmo001 @ludeeus
/homeassistant/components/glances/ @engrbm87 /homeassistant/components/glances/ @engrbm87
/tests/components/glances/ @engrbm87 /tests/components/glances/ @engrbm87
/homeassistant/components/go2rtc/ @home-assistant/core
/tests/components/go2rtc/ @home-assistant/core
/homeassistant/components/goalzero/ @tkdrob /homeassistant/components/goalzero/ @tkdrob
/tests/components/goalzero/ @tkdrob /tests/components/goalzero/ @tkdrob
/homeassistant/components/gogogate2/ @vangorra /homeassistant/components/gogogate2/ @vangorra
@ -615,8 +617,8 @@ build.json @home-assistant/supervisor
/tests/components/hlk_sw16/ @jameshilliard /tests/components/hlk_sw16/ @jameshilliard
/homeassistant/components/holiday/ @jrieger @gjohansson-ST /homeassistant/components/holiday/ @jrieger @gjohansson-ST
/tests/components/holiday/ @jrieger @gjohansson-ST /tests/components/holiday/ @jrieger @gjohansson-ST
/homeassistant/components/home_connect/ @DavidMStraub /homeassistant/components/home_connect/ @DavidMStraub @Diegorro98
/tests/components/home_connect/ @DavidMStraub /tests/components/home_connect/ @DavidMStraub @Diegorro98
/homeassistant/components/homeassistant/ @home-assistant/core /homeassistant/components/homeassistant/ @home-assistant/core
/tests/components/homeassistant/ @home-assistant/core /tests/components/homeassistant/ @home-assistant/core
/homeassistant/components/homeassistant_alerts/ @home-assistant/core /homeassistant/components/homeassistant_alerts/ @home-assistant/core
@ -657,6 +659,8 @@ build.json @home-assistant/supervisor
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock /tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/homeassistant/components/husqvarna_automower/ @Thomas55555 /homeassistant/components/husqvarna_automower/ @Thomas55555
/tests/components/husqvarna_automower/ @Thomas55555 /tests/components/husqvarna_automower/ @Thomas55555
/homeassistant/components/husqvarna_automower_ble/ @alistair23
/tests/components/husqvarna_automower_ble/ @alistair23
/homeassistant/components/huum/ @frwickst /homeassistant/components/huum/ @frwickst
/tests/components/huum/ @frwickst /tests/components/huum/ @frwickst
/homeassistant/components/hvv_departures/ @vigonotion /homeassistant/components/hvv_departures/ @vigonotion
@ -817,6 +821,8 @@ build.json @home-assistant/supervisor
/tests/components/lektrico/ @lektrico /tests/components/lektrico/ @lektrico
/homeassistant/components/lg_netcast/ @Drafteed @splinter98 /homeassistant/components/lg_netcast/ @Drafteed @splinter98
/tests/components/lg_netcast/ @Drafteed @splinter98 /tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
/tests/components/lg_thinq/ @LG-ThinQ-Integration
/homeassistant/components/lidarr/ @tkdrob /homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob /tests/components/lidarr/ @tkdrob
/homeassistant/components/lifx/ @Djelibeybi /homeassistant/components/lifx/ @Djelibeybi
@ -1045,6 +1051,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/onewire/ @garbled1 @epenet /homeassistant/components/onewire/ @garbled1 @epenet
/tests/components/onewire/ @garbled1 @epenet /tests/components/onewire/ @garbled1 @epenet
/homeassistant/components/onkyo/ @arturpragacz /homeassistant/components/onkyo/ @arturpragacz
/tests/components/onkyo/ @arturpragacz
/homeassistant/components/onvif/ @hunterjm /homeassistant/components/onvif/ @hunterjm
/tests/components/onvif/ @hunterjm /tests/components/onvif/ @hunterjm
/homeassistant/components/open_meteo/ @frenck /homeassistant/components/open_meteo/ @frenck
@ -1086,10 +1093,10 @@ build.json @home-assistant/supervisor
/tests/components/ovo_energy/ @timmo001 /tests/components/ovo_energy/ @timmo001
/homeassistant/components/p1_monitor/ @klaasnicolaas /homeassistant/components/p1_monitor/ @klaasnicolaas
/tests/components/p1_monitor/ @klaasnicolaas /tests/components/p1_monitor/ @klaasnicolaas
/homeassistant/components/palazzetti/ @dotvav
/tests/components/palazzetti/ @dotvav
/homeassistant/components/panel_custom/ @home-assistant/frontend /homeassistant/components/panel_custom/ @home-assistant/frontend
/tests/components/panel_custom/ @home-assistant/frontend /tests/components/panel_custom/ @home-assistant/frontend
/homeassistant/components/panel_iframe/ @home-assistant/frontend
/tests/components/panel_iframe/ @home-assistant/frontend
/homeassistant/components/peco/ @IceBotYT /homeassistant/components/peco/ @IceBotYT
/tests/components/peco/ @IceBotYT /tests/components/peco/ @IceBotYT
/homeassistant/components/pegel_online/ @mib1185 /homeassistant/components/pegel_online/ @mib1185
@ -1237,8 +1244,8 @@ build.json @home-assistant/supervisor
/tests/components/roku/ @ctalkington /tests/components/roku/ @ctalkington
/homeassistant/components/romy/ @xeniter /homeassistant/components/romy/ @xeniter
/tests/components/romy/ @xeniter /tests/components/romy/ @xeniter
/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn @Xitee1 @Orhideous /homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Xitee1 @Orhideous /tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
/homeassistant/components/roon/ @pavoni /homeassistant/components/roon/ @pavoni
/tests/components/roon/ @pavoni /tests/components/roon/ @pavoni
/homeassistant/components/rpi_power/ @shenxn @swetoast /homeassistant/components/rpi_power/ @shenxn @swetoast
@ -1349,6 +1356,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/smarttub/ @mdz /homeassistant/components/smarttub/ @mdz
/tests/components/smarttub/ @mdz /tests/components/smarttub/ @mdz
/homeassistant/components/smarty/ @z0mbieprocess /homeassistant/components/smarty/ @z0mbieprocess
/tests/components/smarty/ @z0mbieprocess
/homeassistant/components/smhi/ @gjohansson-ST /homeassistant/components/smhi/ @gjohansson-ST
/tests/components/smhi/ @gjohansson-ST /tests/components/smhi/ @gjohansson-ST
/homeassistant/components/smlight/ @tl-sl /homeassistant/components/smlight/ @tl-sl
@ -1382,15 +1390,13 @@ build.json @home-assistant/supervisor
/tests/components/spaceapi/ @fabaff /tests/components/spaceapi/ @fabaff
/homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87 /homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87
/tests/components/speedtestdotnet/ @rohankapoorcom @engrbm87 /tests/components/speedtestdotnet/ @rohankapoorcom @engrbm87
/homeassistant/components/spider/ @peternijssen
/tests/components/spider/ @peternijssen
/homeassistant/components/splunk/ @Bre77 /homeassistant/components/splunk/ @Bre77
/homeassistant/components/spotify/ @frenck @joostlek /homeassistant/components/spotify/ @frenck @joostlek
/tests/components/spotify/ @frenck @joostlek /tests/components/spotify/ @frenck @joostlek
/homeassistant/components/sql/ @gjohansson-ST @dougiteixeira /homeassistant/components/sql/ @gjohansson-ST @dougiteixeira
/tests/components/sql/ @gjohansson-ST @dougiteixeira /tests/components/sql/ @gjohansson-ST @dougiteixeira
/homeassistant/components/squeezebox/ @rajlaud /homeassistant/components/squeezebox/ @rajlaud @pssc @peteS-UK
/tests/components/squeezebox/ @rajlaud /tests/components/squeezebox/ @rajlaud @pssc @peteS-UK
/homeassistant/components/srp_energy/ @briglx /homeassistant/components/srp_energy/ @briglx
/tests/components/srp_energy/ @briglx /tests/components/srp_energy/ @briglx
/homeassistant/components/starline/ @anonym-tsk /homeassistant/components/starline/ @anonym-tsk
@ -1414,8 +1420,8 @@ build.json @home-assistant/supervisor
/tests/components/stt/ @home-assistant/core /tests/components/stt/ @home-assistant/core
/homeassistant/components/subaru/ @G-Two /homeassistant/components/subaru/ @G-Two
/tests/components/subaru/ @G-Two /tests/components/subaru/ @G-Two
/homeassistant/components/suez_water/ @ooii /homeassistant/components/suez_water/ @ooii @jb101010-2
/tests/components/suez_water/ @ooii /tests/components/suez_water/ @ooii @jb101010-2
/homeassistant/components/sun/ @Swamp-Ig /homeassistant/components/sun/ @Swamp-Ig
/tests/components/sun/ @Swamp-Ig /tests/components/sun/ @Swamp-Ig
/homeassistant/components/sunweg/ @rokam /homeassistant/components/sunweg/ @rokam

View File

@ -7,12 +7,13 @@ FROM ${BUILD_FROM}
# Synchronize with homeassistant/core.py:async_stop # Synchronize with homeassistant/core.py:async_stop
ENV \ ENV \
S6_SERVICES_GRACETIME=240000 \ S6_SERVICES_GRACETIME=240000 \
UV_SYSTEM_PYTHON=true UV_SYSTEM_PYTHON=true \
UV_NO_CACHE=true
ARG QEMU_CPU ARG QEMU_CPU
# Install uv # Install uv
RUN pip3 install uv==0.4.15 RUN pip3 install uv==0.4.28
WORKDIR /usr/src WORKDIR /usr/src
@ -44,4 +45,19 @@ RUN \
# Home Assistant S6-Overlay # Home Assistant S6-Overlay
COPY rootfs / COPY rootfs /
# Needs to be redefined inside the FROM statement to be set for RUN commands
ARG BUILD_ARCH
# Get go2rtc binary
RUN \
case "${BUILD_ARCH}" in \
"aarch64") go2rtc_suffix='arm64' ;; \
"armhf") go2rtc_suffix='armv6' ;; \
"armv7") go2rtc_suffix='arm' ;; \
*) go2rtc_suffix=${BUILD_ARCH} ;; \
esac \
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.6/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& chmod +x /bin/go2rtc \
# Verify go2rtc can be executed
&& go2rtc --version
WORKDIR /config WORKDIR /config

View File

@ -7,8 +7,6 @@ Check out `home-assistant.io <https://home-assistant.io>`__ for `a
demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__, demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__. `tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
This is a project of the `Open Home Foundation <https://www.openhomefoundation.org/>`__.
|screenshot-states| |screenshot-states|
Featured integrations Featured integrations
@ -22,9 +20,14 @@ components <https://developers.home-assistant.io/docs/creating_component_index/>
If you run into issues while using Home Assistant or during development If you run into issues while using Home Assistant or during development
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information. of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|ohf-logo|
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg .. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
:target: https://www.home-assistant.io/join-chat/ :target: https://www.home-assistant.io/join-chat/
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png .. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png
:target: https://demo.home-assistant.io :target: https://demo.home-assistant.io
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png .. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
:target: https://home-assistant.io/integrations/ :target: https://home-assistant.io/integrations/
.. |ohf-logo| image:: https://www.openhomefoundation.org/badges/home-assistant.png
:alt: Home Assistant - A project from the Open Home Foundation
:target: https://www.openhomefoundation.org/

View File

@ -12,7 +12,6 @@ from typing import Any, cast
import jwt import jwt
from homeassistant import data_entry_flow
from homeassistant.core import ( from homeassistant.core import (
CALLBACK_TYPE, CALLBACK_TYPE,
HassJob, HassJob,
@ -20,13 +19,14 @@ from homeassistant.core import (
HomeAssistant, HomeAssistant,
callback, callback,
) )
from homeassistant.data_entry_flow import FlowHandler, FlowManager, FlowResultType
from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import auth_store, jwt_wrapper, models from . import auth_store, jwt_wrapper, models
from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRATION from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRATION
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
from .models import AuthFlowResult from .models import AuthFlowContext, AuthFlowResult
from .providers import AuthProvider, LoginFlow, auth_provider_from_config from .providers import AuthProvider, LoginFlow, auth_provider_from_config
from .providers.homeassistant import HassAuthProvider from .providers.homeassistant import HassAuthProvider
@ -98,7 +98,7 @@ async def auth_manager_from_config(
class AuthManagerFlowManager( class AuthManagerFlowManager(
data_entry_flow.FlowManager[AuthFlowResult, tuple[str, str]] FlowManager[AuthFlowContext, AuthFlowResult, tuple[str, str]]
): ):
"""Manage authentication flows.""" """Manage authentication flows."""
@ -113,7 +113,7 @@ class AuthManagerFlowManager(
self, self,
handler_key: tuple[str, str], handler_key: tuple[str, str],
*, *,
context: dict[str, Any] | None = None, context: AuthFlowContext | None = None,
data: dict[str, Any] | None = None, data: dict[str, Any] | None = None,
) -> LoginFlow: ) -> LoginFlow:
"""Create a login flow.""" """Create a login flow."""
@ -124,7 +124,7 @@ class AuthManagerFlowManager(
async def async_finish_flow( async def async_finish_flow(
self, self,
flow: data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]], flow: FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]],
result: AuthFlowResult, result: AuthFlowResult,
) -> AuthFlowResult: ) -> AuthFlowResult:
"""Return a user as result of login flow. """Return a user as result of login flow.
@ -134,7 +134,7 @@ class AuthManagerFlowManager(
""" """
flow = cast(LoginFlow, flow) flow = cast(LoginFlow, flow)
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: if result["type"] != FlowResultType.CREATE_ENTRY:
return result return result
# we got final result # we got final result

View File

@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import cached_property from ipaddress import IPv4Address, IPv6Address
import secrets import secrets
from typing import Any, NamedTuple from typing import Any, NamedTuple
import uuid import uuid
@ -11,9 +11,10 @@ import uuid
import attr import attr
from attr import Attribute from attr import Attribute
from attr.setters import validate from attr.setters import validate
from propcache import cached_property
from homeassistant.const import __version__ from homeassistant.const import __version__
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowContext, FlowResult
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import permissions as perm_mdl from . import permissions as perm_mdl
@ -23,7 +24,16 @@ TOKEN_TYPE_NORMAL = "normal"
TOKEN_TYPE_SYSTEM = "system" TOKEN_TYPE_SYSTEM = "system"
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token" TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
AuthFlowResult = FlowResult[tuple[str, str]]
class AuthFlowContext(FlowContext, total=False):
"""Typed context dict for auth flow."""
credential_only: bool
ip_address: IPv4Address | IPv6Address
redirect_uri: str
AuthFlowResult = FlowResult[AuthFlowContext, tuple[str, str]]
@attr.s(slots=True) @attr.s(slots=True)

View File

@ -10,9 +10,10 @@ from typing import Any
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from homeassistant import data_entry_flow, requirements from homeassistant import requirements
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowHandler
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.importlib import async_import_module from homeassistant.helpers.importlib import async_import_module
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -21,7 +22,14 @@ from homeassistant.util.hass_dict import HassKey
from ..auth_store import AuthStore from ..auth_store import AuthStore
from ..const import MFA_SESSION_EXPIRATION from ..const import MFA_SESSION_EXPIRATION
from ..models import AuthFlowResult, Credentials, RefreshToken, User, UserMeta from ..models import (
AuthFlowContext,
AuthFlowResult,
Credentials,
RefreshToken,
User,
UserMeta,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_REQS: HassKey[set[str]] = HassKey("auth_prov_reqs_processed") DATA_REQS: HassKey[set[str]] = HassKey("auth_prov_reqs_processed")
@ -97,7 +105,7 @@ class AuthProvider:
# Implement by extending class # Implement by extending class
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
"""Return the data flow for logging in with auth provider. """Return the data flow for logging in with auth provider.
Auth provider should extend LoginFlow and return an instance. Auth provider should extend LoginFlow and return an instance.
@ -184,7 +192,7 @@ async def load_auth_provider_module(
return module return module
class LoginFlow(data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]]): class LoginFlow(FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]]):
"""Handler for the login flow.""" """Handler for the login flow."""
_flow_result = AuthFlowResult _flow_result = AuthFlowResult

View File

@ -13,7 +13,7 @@ import voluptuous as vol
from homeassistant.const import CONF_COMMAND from homeassistant.const import CONF_COMMAND
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from ..models import AuthFlowResult, Credentials, UserMeta from ..models import AuthFlowContext, AuthFlowResult, Credentials, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
CONF_ARGS = "args" CONF_ARGS = "args"
@ -59,7 +59,7 @@ class CommandLineAuthProvider(AuthProvider):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._user_meta: dict[str, dict[str, Any]] = {} self._user_meta: dict[str, dict[str, Any]] = {}
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
"""Return a flow to login.""" """Return a flow to login."""
return CommandLineLoginFlow(self) return CommandLineLoginFlow(self)

View File

@ -17,7 +17,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import issue_registry as ir from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from ..models import AuthFlowResult, Credentials, UserMeta from ..models import AuthFlowContext, AuthFlowResult, Credentials, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
STORAGE_VERSION = 1 STORAGE_VERSION = 1
@ -305,7 +305,7 @@ class HassAuthProvider(AuthProvider):
await data.async_load() await data.async_load()
self.data = data self.data = data
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
"""Return a flow to login.""" """Return a flow to login."""
return HassLoginFlow(self) return HassLoginFlow(self)

View File

@ -4,14 +4,14 @@ from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
import hmac import hmac
from typing import Any, cast from typing import cast
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from ..models import AuthFlowResult, Credentials, UserMeta from ..models import AuthFlowContext, AuthFlowResult, Credentials, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
USER_SCHEMA = vol.Schema( USER_SCHEMA = vol.Schema(
@ -36,7 +36,7 @@ class InvalidAuthError(HomeAssistantError):
class ExampleAuthProvider(AuthProvider): class ExampleAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords.""" """Example auth provider based on hardcoded usernames and passwords."""
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
"""Return a flow to login.""" """Return a flow to login."""
return ExampleLoginFlow(self) return ExampleLoginFlow(self)

View File

@ -25,7 +25,13 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.network import is_cloud_connection from homeassistant.helpers.network import is_cloud_connection
from .. import InvalidAuthError from .. import InvalidAuthError
from ..models import AuthFlowResult, Credentials, RefreshToken, UserMeta from ..models import (
AuthFlowContext,
AuthFlowResult,
Credentials,
RefreshToken,
UserMeta,
)
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
type IPAddress = IPv4Address | IPv6Address type IPAddress = IPv4Address | IPv6Address
@ -98,7 +104,7 @@ class TrustedNetworksAuthProvider(AuthProvider):
"""Trusted Networks auth provider does not support MFA.""" """Trusted Networks auth provider does not support MFA."""
return False return False
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow: async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
"""Return a flow to login.""" """Return a flow to login."""
assert context is not None assert context is not None
ip_addr = cast(IPAddress, context.get("ip_address")) ip_addr = cast(IPAddress, context.get("ip_address"))

View File

@ -9,6 +9,7 @@ import it.
from __future__ import annotations from __future__ import annotations
# pylint: disable-next=hass-deprecated-import
from functools import cached_property as _cached_property, partial from functools import cached_property as _cached_property, partial
from homeassistant.helpers.deprecation import ( from homeassistant.helpers.deprecation import (

View File

@ -70,6 +70,7 @@ from .const import (
REQUIRED_NEXT_PYTHON_VER, REQUIRED_NEXT_PYTHON_VER,
SIGNAL_BOOTSTRAP_INTEGRATIONS, SIGNAL_BOOTSTRAP_INTEGRATIONS,
) )
from .core_config import async_process_ha_core_config
from .exceptions import HomeAssistantError from .exceptions import HomeAssistantError
from .helpers import ( from .helpers import (
area_registry, area_registry,
@ -479,7 +480,7 @@ async def async_from_config_dict(
core_config = config.get(core.DOMAIN, {}) core_config = config.get(core.DOMAIN, {})
try: try:
await conf_util.async_process_ha_core_config(hass, core_config) await async_process_ha_core_config(hass, core_config)
except vol.Invalid as config_err: except vol.Invalid as config_err:
conf_util.async_log_schema_error(config_err, core.DOMAIN, core_config, hass) conf_util.async_log_schema_error(config_err, core.DOMAIN, core_config, hass)
async_notify_setup_error(hass, core.DOMAIN) async_notify_setup_error(hass, core.DOMAIN)

View File

@ -5,7 +5,6 @@
"google_assistant", "google_assistant",
"google_assistant_sdk", "google_assistant_sdk",
"google_cloud", "google_cloud",
"google_domains",
"google_generative_ai_conversation", "google_generative_ai_conversation",
"google_mail", "google_mail",
"google_maps", "google_maps",

View File

@ -0,0 +1,5 @@
{
"domain": "husqvarna",
"name": "Husqvarna",
"integrations": ["husqvarna_automower", "husqvarna_automower_ble"]
}

View File

@ -1,5 +1,5 @@
{ {
"domain": "lg", "domain": "lg",
"name": "LG", "name": "LG",
"integrations": ["lg_netcast", "lg_soundbar", "webostv"] "integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
} }

View File

@ -7,13 +7,9 @@ from jaraco.abode.devices.alarm import Alarm
from homeassistant.components.alarm_control_panel import ( from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity, AlarmControlPanelEntity,
AlarmControlPanelEntityFeature, AlarmControlPanelEntityFeature,
AlarmControlPanelState,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -44,14 +40,14 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanelEntity):
_device: Alarm _device: Alarm
@property @property
def state(self) -> str | None: def alarm_state(self) -> AlarmControlPanelState | None:
"""Return the state of the device.""" """Return the state of the device."""
if self._device.is_standby: if self._device.is_standby:
return STATE_ALARM_DISARMED return AlarmControlPanelState.DISARMED
if self._device.is_away: if self._device.is_away:
return STATE_ALARM_ARMED_AWAY return AlarmControlPanelState.ARMED_AWAY
if self._device.is_home: if self._device.is_home:
return STATE_ALARM_ARMED_HOME return AlarmControlPanelState.ARMED_HOME
return None return None
def alarm_disarm(self, code: str | None = None) -> None: def alarm_disarm(self, code: str | None = None) -> None:

View File

@ -102,15 +102,7 @@ class AbodeFlowHandler(ConfigFlow, domain=DOMAIN):
existing_entry = await self.async_set_unique_id(self._username) existing_entry = await self.async_set_unique_id(self._username)
if existing_entry: if existing_entry:
self.hass.config_entries.async_update_entry( return self.async_update_reload_and_abort(existing_entry, data=config_data)
existing_entry, data=config_data
)
# Reload the Abode config entry otherwise devices will remain unavailable
self.hass.async_create_task(
self.hass.config_entries.async_reload(existing_entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry( return self.async_create_entry(
title=cast(str, self._username), data=config_data title=cast(str, self._username), data=config_data

View File

@ -2,13 +2,11 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
import logging import logging
from accuweather import AccuWeather from accuweather import AccuWeather
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -16,7 +14,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, UPDATE_INTERVAL_DAILY_FORECAST, UPDATE_INTERVAL_OBSERVATION from .const import DOMAIN, UPDATE_INTERVAL_DAILY_FORECAST, UPDATE_INTERVAL_OBSERVATION
from .coordinator import ( from .coordinator import (
AccuWeatherConfigEntry,
AccuWeatherDailyForecastDataUpdateCoordinator, AccuWeatherDailyForecastDataUpdateCoordinator,
AccuWeatherData,
AccuWeatherObservationDataUpdateCoordinator, AccuWeatherObservationDataUpdateCoordinator,
) )
@ -25,17 +25,6 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR, Platform.WEATHER] PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
@dataclass
class AccuWeatherData:
"""Data for AccuWeather integration."""
coordinator_observation: AccuWeatherObservationDataUpdateCoordinator
coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
type AccuWeatherConfigEntry = ConfigEntry[AccuWeatherData]
async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) -> bool:
"""Set up AccuWeather as config entry.""" """Set up AccuWeather as config entry."""
api_key: str = entry.data[CONF_API_KEY] api_key: str = entry.data[CONF_API_KEY]
@ -50,6 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry)
coordinator_observation = AccuWeatherObservationDataUpdateCoordinator( coordinator_observation = AccuWeatherObservationDataUpdateCoordinator(
hass, hass,
entry,
accuweather, accuweather,
name, name,
"observation", "observation",
@ -58,6 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry)
coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator( coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator(
hass, hass,
entry,
accuweather, accuweather,
name, name,
"daily forecast", "daily forecast",

View File

@ -1,6 +1,9 @@
"""The AccuWeather coordinator.""" """The AccuWeather coordinator."""
from __future__ import annotations
from asyncio import timeout from asyncio import timeout
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
@ -8,6 +11,7 @@ from typing import TYPE_CHECKING, Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
@ -23,6 +27,17 @@ EXCEPTIONS = (ApiError, ClientConnectorError, InvalidApiKeyError, RequestsExceed
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@dataclass
class AccuWeatherData:
"""Data for AccuWeather integration."""
coordinator_observation: AccuWeatherObservationDataUpdateCoordinator
coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
type AccuWeatherConfigEntry = ConfigEntry[AccuWeatherData]
class AccuWeatherObservationDataUpdateCoordinator( class AccuWeatherObservationDataUpdateCoordinator(
DataUpdateCoordinator[dict[str, Any]] DataUpdateCoordinator[dict[str, Any]]
): ):
@ -31,6 +46,7 @@ class AccuWeatherObservationDataUpdateCoordinator(
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AccuWeatherConfigEntry,
accuweather: AccuWeather, accuweather: AccuWeather,
name: str, name: str,
coordinator_type: str, coordinator_type: str,
@ -48,6 +64,7 @@ class AccuWeatherObservationDataUpdateCoordinator(
super().__init__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
config_entry=config_entry,
name=f"{name} ({coordinator_type})", name=f"{name} ({coordinator_type})",
update_interval=update_interval, update_interval=update_interval,
) )
@ -73,6 +90,7 @@ class AccuWeatherDailyForecastDataUpdateCoordinator(
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AccuWeatherConfigEntry,
accuweather: AccuWeather, accuweather: AccuWeather,
name: str, name: str,
coordinator_type: str, coordinator_type: str,
@ -90,6 +108,7 @@ class AccuWeatherDailyForecastDataUpdateCoordinator(
super().__init__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
config_entry=config_entry,
name=f"{name} ({coordinator_type})", name=f"{name} ({coordinator_type})",
update_interval=update_interval, update_interval=update_interval,
) )

View File

@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import AccuWeatherConfigEntry, AccuWeatherData from .coordinator import AccuWeatherConfigEntry, AccuWeatherData
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE} TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE}

View File

@ -28,7 +28,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AccuWeatherConfigEntry
from .const import ( from .const import (
API_METRIC, API_METRIC,
ATTR_CATEGORY, ATTR_CATEGORY,
@ -41,6 +40,7 @@ from .const import (
MAX_FORECAST_DAYS, MAX_FORECAST_DAYS,
) )
from .coordinator import ( from .coordinator import (
AccuWeatherConfigEntry,
AccuWeatherDailyForecastDataUpdateCoordinator, AccuWeatherDailyForecastDataUpdateCoordinator,
AccuWeatherObservationDataUpdateCoordinator, AccuWeatherObservationDataUpdateCoordinator,
) )

View File

@ -9,8 +9,8 @@ from accuweather.const import ENDPOINT
from homeassistant.components import system_health from homeassistant.components import system_health
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from . import AccuWeatherConfigEntry
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AccuWeatherConfigEntry
@callback @callback

View File

@ -33,7 +33,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.dt import utc_from_timestamp
from . import AccuWeatherConfigEntry, AccuWeatherData
from .const import ( from .const import (
API_METRIC, API_METRIC,
ATTR_DIRECTION, ATTR_DIRECTION,
@ -43,7 +42,9 @@ from .const import (
CONDITION_MAP, CONDITION_MAP,
) )
from .coordinator import ( from .coordinator import (
AccuWeatherConfigEntry,
AccuWeatherDailyForecastDataUpdateCoordinator, AccuWeatherDailyForecastDataUpdateCoordinator,
AccuWeatherData,
AccuWeatherObservationDataUpdateCoordinator, AccuWeatherObservationDataUpdateCoordinator,
) )

View File

@ -7,7 +7,6 @@ from typing import Any
from adguardhome import AdGuardHome, AdGuardHomeConnectionError from adguardhome import AdGuardHome, AdGuardHomeConnectionError
import voluptuous as vol import voluptuous as vol
from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@ -18,6 +17,7 @@ from homeassistant.const import (
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
) )
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from .const import DOMAIN from .const import DOMAIN

View File

@ -55,6 +55,7 @@ async def async_setup_entry(
coordinator = DataUpdateCoordinator( coordinator = DataUpdateCoordinator(
hass, hass,
_LOGGER, _LOGGER,
config_entry=entry,
name="Advantage Air", name="Advantage Air",
update_method=async_get, update_method=async_get,
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL), update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),

View File

@ -249,6 +249,7 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
name="Rain", name="Rain",
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
state_class=SensorStateClass.MEASUREMENT,
), ),
AemetSensorEntityDescription( AemetSensorEntityDescription(
key=ATTR_API_RAIN_PROB, key=ATTR_API_RAIN_PROB,
@ -263,6 +264,7 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
name="Snow", name="Snow",
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
state_class=SensorStateClass.MEASUREMENT,
), ),
AemetSensorEntityDescription( AemetSensorEntityDescription(
key=ATTR_API_SNOW_PROB, key=ATTR_API_SNOW_PROB,

View File

@ -5,12 +5,7 @@ from __future__ import annotations
from homeassistant.components.alarm_control_panel import ( from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity, AlarmControlPanelEntity,
AlarmControlPanelEntityFeature, AlarmControlPanelEntityFeature,
) AlarmControlPanelState,
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
@ -65,37 +60,37 @@ class AgentBaseStation(AlarmControlPanelEntity):
self._attr_available = self._client.is_available self._attr_available = self._client.is_available
armed = self._client.is_armed armed = self._client.is_armed
if armed is None: if armed is None:
self._attr_state = None self._attr_alarm_state = None
return return
if armed: if armed:
prof = (await self._client.get_active_profile()).lower() prof = (await self._client.get_active_profile()).lower()
self._attr_state = STATE_ALARM_ARMED_AWAY self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
if prof == CONF_HOME_MODE_NAME: if prof == CONF_HOME_MODE_NAME:
self._attr_state = STATE_ALARM_ARMED_HOME self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
elif prof == CONF_NIGHT_MODE_NAME: elif prof == CONF_NIGHT_MODE_NAME:
self._attr_state = STATE_ALARM_ARMED_NIGHT self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
else: else:
self._attr_state = STATE_ALARM_DISARMED self._attr_alarm_state = AlarmControlPanelState.DISARMED
async def async_alarm_disarm(self, code: str | None = None) -> None: async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.""" """Send disarm command."""
await self._client.disarm() await self._client.disarm()
self._attr_state = STATE_ALARM_DISARMED self._attr_alarm_state = AlarmControlPanelState.DISARMED
async def async_alarm_arm_away(self, code: str | None = None) -> None: async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command. Uses custom mode.""" """Send arm away command. Uses custom mode."""
await self._client.arm() await self._client.arm()
await self._client.set_active_profile(CONF_AWAY_MODE_NAME) await self._client.set_active_profile(CONF_AWAY_MODE_NAME)
self._attr_state = STATE_ALARM_ARMED_AWAY self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
async def async_alarm_arm_home(self, code: str | None = None) -> None: async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command. Uses custom mode.""" """Send arm home command. Uses custom mode."""
await self._client.arm() await self._client.arm()
await self._client.set_active_profile(CONF_HOME_MODE_NAME) await self._client.set_active_profile(CONF_HOME_MODE_NAME)
self._attr_state = STATE_ALARM_ARMED_HOME self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
async def async_alarm_arm_night(self, code: str | None = None) -> None: async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command. Uses custom mode.""" """Send arm night command. Uses custom mode."""
await self._client.arm() await self._client.arm()
await self._client.set_active_profile(CONF_NIGHT_MODE_NAME) await self._client.set_active_profile(CONF_NIGHT_MODE_NAME)
self._attr_state = STATE_ALARM_ARMED_NIGHT self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT

View File

@ -1,7 +1,8 @@
"""Airgradient Update platform.""" """Airgradient Update platform."""
from datetime import timedelta from datetime import timedelta
from functools import cached_property
from propcache import cached_property
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant

View File

@ -42,6 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
coordinator = DataUpdateCoordinator( coordinator = DataUpdateCoordinator(
hass, hass,
_LOGGER, _LOGGER,
config_entry=entry,
name=DOMAIN, name=DOMAIN,
update_method=_update_method, update_method=_update_method,
update_interval=SCAN_INTERVAL, update_interval=SCAN_INTERVAL,

View File

@ -2,75 +2,27 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
import logging
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
from bleak_retry_connector import close_stale_connections_by_address
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.unit_system import METRIC_SYSTEM
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MAX_RETRIES_AFTER_STARTUP from .const import MAX_RETRIES_AFTER_STARTUP
from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator[AirthingsDevice]
AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator]
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: AirthingsBLEConfigEntry hass: HomeAssistant, entry: AirthingsBLEConfigEntry
) -> bool: ) -> bool:
"""Set up Airthings BLE device from a config entry.""" """Set up Airthings BLE device from a config entry."""
hass.data.setdefault(DOMAIN, {}) coordinator = AirthingsBLEDataUpdateCoordinator(hass, entry)
address = entry.unique_id
is_metric = hass.config.units is METRIC_SYSTEM
assert address is not None
await close_stale_connections_by_address(address)
ble_device = bluetooth.async_ble_device_from_address(hass, address)
if not ble_device:
raise ConfigEntryNotReady(
f"Could not find Airthings device with address {address}"
)
airthings = AirthingsBluetoothDeviceData(_LOGGER, is_metric)
async def _async_update_method() -> AirthingsDevice:
"""Get data from Airthings BLE."""
try:
data = await airthings.update_device(ble_device)
except Exception as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
return data
coordinator: AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=DOMAIN,
update_method=_async_update_method,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
# Once its setup and we know we are not going to delay # Once its setup and we know we are not going to delay
# the startup of Home Assistant, we can set the max attempts # the startup of Home Assistant, we can set the max attempts
# to a higher value. If the first connection attempt fails, # to a higher value. If the first connection attempt fails,
# Home Assistant's built-in retry logic will take over. # Home Assistant's built-in retry logic will take over.
airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP) coordinator.airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP)
entry.runtime_data = coordinator entry.runtime_data = coordinator

View File

@ -0,0 +1,68 @@
"""The Airthings BLE integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
from bleak.backends.device import BLEDevice
from bleak_retry_connector import close_stale_connections_by_address
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.unit_system import METRIC_SYSTEM
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
type AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator]
class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
"""Class to manage fetching Airthings BLE data."""
ble_device: BLEDevice
config_entry: AirthingsBLEConfigEntry
def __init__(self, hass: HomeAssistant, entry: AirthingsBLEConfigEntry) -> None:
"""Initialize the coordinator."""
self.airthings = AirthingsBluetoothDeviceData(
_LOGGER, hass.config.units is METRIC_SYSTEM
)
super().__init__(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
async def _async_setup(self) -> None:
"""Set up the coordinator."""
address = self.config_entry.unique_id
assert address is not None
await close_stale_connections_by_address(address)
ble_device = bluetooth.async_ble_device_from_address(self.hass, address)
if not ble_device:
raise ConfigEntryNotReady(
f"Could not find Airthings device with address {address}"
)
self.ble_device = ble_device
async def _async_update_data(self) -> AirthingsDevice:
"""Get data from Airthings BLE."""
try:
data = await self.airthings.update_device(self.ble_device)
except Exception as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
return data

View File

@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble", "documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["airthings-ble==0.9.1"] "requirements": ["airthings-ble==0.9.2"]
} }

View File

@ -34,8 +34,8 @@ from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.unit_system import METRIC_SYSTEM
from . import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE
from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -9,8 +9,6 @@ from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER] PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER]
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient] type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
@ -19,8 +17,6 @@ type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
async def async_setup_entry(hass: HomeAssistant, entry: Airtouch5ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: Airtouch5ConfigEntry) -> bool:
"""Set up Airtouch 5 from a config entry.""" """Set up Airtouch 5 from a config entry."""
hass.data.setdefault(DOMAIN, {})
# Create API instance # Create API instance
host = entry.data[CONF_HOST] host = entry.data[CONF_HOST]
client = Airtouch5SimpleClient(host) client = Airtouch5SimpleClient(host)

View File

@ -204,6 +204,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
coordinator = DataUpdateCoordinator( coordinator = DataUpdateCoordinator(
hass, hass,
LOGGER, LOGGER,
config_entry=entry,
name=async_get_geography_id(entry.data), name=async_get_geography_id(entry.data),
# We give a placeholder update interval in order to create the coordinator; # We give a placeholder update interval in order to create the coordinator;
# then, below, we use the coordinator's presence (along with any other # then, below, we use the coordinator's presence (along with any other

View File

@ -16,7 +16,12 @@ from pyairvisual.cloud_api import (
from pyairvisual.errors import AirVisualError from pyairvisual.errors import AirVisualError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.config_entries import (
SOURCE_REAUTH,
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_COUNTRY, CONF_COUNTRY,
@ -140,12 +145,11 @@ class AirVisualFlowHandler(ConfigFlow, domain=DOMAIN):
valid_keys.add(user_input[CONF_API_KEY]) valid_keys.add(user_input[CONF_API_KEY])
if existing_entry := await self.async_set_unique_id(self._geo_id): if self.source == SOURCE_REAUTH:
self.hass.config_entries.async_update_entry(existing_entry, data=user_input) return self.async_update_reload_and_abort(
self.hass.async_create_task( self._get_reauth_entry(),
self.hass.config_entries.async_reload(existing_entry.entry_id) data_updates={CONF_API_KEY: user_input[CONF_API_KEY]},
) )
return self.async_abort(reason="reauth_successful")
return self.async_create_entry( return self.async_create_entry(
title=f"Cloud API ({self._geo_id})", title=f"Cloud API ({self._geo_id})",

View File

@ -32,7 +32,7 @@
} }
}, },
"error": { "error": {
"general_error": "[%key:common::config_flow::error::unknown%]", "unknown": "[%key:common::config_flow::error::unknown%]",
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"location_not_found": "Location not found", "location_not_found": "Location not found",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"

View File

@ -81,6 +81,7 @@ async def async_setup_entry(
coordinator = DataUpdateCoordinator( coordinator = DataUpdateCoordinator(
hass, hass,
LOGGER, LOGGER,
config_entry=entry,
name="Node/Pro data", name="Node/Pro data",
update_interval=UPDATE_INTERVAL, update_interval=UPDATE_INTERVAL,
update_method=async_get_data, update_method=async_get_data,

View File

@ -14,7 +14,7 @@ from pyairvisual.node import (
) )
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
@ -76,9 +76,7 @@ class AirVisualProFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
def __init__(self) -> None: _reauth_entry_data: Mapping[str, Any]
"""Initialize."""
self._reauth_entry: ConfigEntry | None = None
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Import a config entry from `airvisual` integration (see #83882).""" """Import a config entry from `airvisual` integration (see #83882)."""
@ -88,9 +86,7 @@ class AirVisualProFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any] self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle configuration by re-auth.""" """Handle configuration by re-auth."""
self._reauth_entry = self.hass.config_entries.async_get_entry( self._reauth_entry_data = entry_data
self.context["entry_id"]
)
return await self.async_step_reauth_confirm() return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm( async def async_step_reauth_confirm(
@ -102,10 +98,8 @@ class AirVisualProFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="reauth_confirm", data_schema=STEP_REAUTH_SCHEMA step_id="reauth_confirm", data_schema=STEP_REAUTH_SCHEMA
) )
assert self._reauth_entry
validation_result = await async_validate_credentials( validation_result = await async_validate_credentials(
self._reauth_entry.data[CONF_IP_ADDRESS], user_input[CONF_PASSWORD] self._reauth_entry_data[CONF_IP_ADDRESS], user_input[CONF_PASSWORD]
) )
if validation_result.errors: if validation_result.errors:
@ -115,13 +109,9 @@ class AirVisualProFlowHandler(ConfigFlow, domain=DOMAIN):
errors=validation_result.errors, errors=validation_result.errors,
) )
self.hass.config_entries.async_update_entry( return self.async_update_reload_and_abort(
self._reauth_entry, data=self._reauth_entry.data | user_input self._get_reauth_entry(), data_updates=user_input
) )
self.hass.async_create_task(
self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
async def async_step_user( async def async_step_user(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None

View File

@ -24,6 +24,7 @@ PLATFORMS: list[Platform] = [
Platform.CLIMATE, Platform.CLIMATE,
Platform.SELECT, Platform.SELECT,
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH,
Platform.WATER_HEATER, Platform.WATER_HEATER,
] ]

View File

@ -275,12 +275,18 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
self._attr_min_temp = self.get_airzone_value(AZD_TEMP_MIN) self._attr_min_temp = self.get_airzone_value(AZD_TEMP_MIN)
if self.supported_features & ClimateEntityFeature.FAN_MODE: if self.supported_features & ClimateEntityFeature.FAN_MODE:
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED)) self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: if (
self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
and self._attr_hvac_mode == HVACMode.HEAT_COOL
):
self._attr_target_temperature_high = self.get_airzone_value( self._attr_target_temperature_high = self.get_airzone_value(
AZD_COOL_TEMP_SET AZD_COOL_TEMP_SET
) )
self._attr_target_temperature_low = self.get_airzone_value( self._attr_target_temperature_low = self.get_airzone_value(
AZD_HEAT_TEMP_SET AZD_HEAT_TEMP_SET
) )
self._attr_target_temperature = None
else: else:
self._attr_target_temperature_high = None
self._attr_target_temperature_low = None
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)

View File

@ -0,0 +1,122 @@
"""Support for the Airzone switch."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Final
from aioairzone.const import API_ON, AZD_ON, AZD_ZONES
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirzoneConfigEntry
from .coordinator import AirzoneUpdateCoordinator
from .entity import AirzoneEntity, AirzoneZoneEntity
@dataclass(frozen=True, kw_only=True)
class AirzoneSwitchDescription(SwitchEntityDescription):
"""Class to describe an Airzone switch entity."""
api_param: str
ZONE_SWITCH_TYPES: Final[tuple[AirzoneSwitchDescription, ...]] = (
AirzoneSwitchDescription(
api_param=API_ON,
device_class=SwitchDeviceClass.SWITCH,
key=AZD_ON,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AirzoneConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone switch from a config_entry."""
coordinator = entry.runtime_data
added_zones: set[str] = set()
def _async_entity_listener() -> None:
"""Handle additions of switch."""
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
async_add_entities(
AirzoneZoneSwitch(
coordinator,
description,
entry,
system_zone_id,
zones_data.get(system_zone_id),
)
for system_zone_id in new_zones
for description in ZONE_SWITCH_TYPES
if description.key in zones_data.get(system_zone_id)
)
added_zones.update(new_zones)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
class AirzoneBaseSwitch(AirzoneEntity, SwitchEntity):
"""Define an Airzone switch."""
entity_description: AirzoneSwitchDescription
@callback
def _handle_coordinator_update(self) -> None:
"""Update attributes when the coordinator updates."""
self._async_update_attrs()
super()._handle_coordinator_update()
@callback
def _async_update_attrs(self) -> None:
"""Update switch attributes."""
self._attr_is_on = self.get_airzone_value(self.entity_description.key)
class AirzoneZoneSwitch(AirzoneZoneEntity, AirzoneBaseSwitch):
"""Define an Airzone Zone switch."""
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
description: AirzoneSwitchDescription,
entry: ConfigEntry,
system_zone_id: str,
zone_data: dict[str, Any],
) -> None:
"""Initialize."""
super().__init__(coordinator, entry, system_zone_id, zone_data)
self._attr_name = None
self._attr_unique_id = (
f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
)
self.entity_description = description
self._async_update_attrs()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
param = self.entity_description.api_param
await self._async_update_hvac_params({param: True})
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
param = self.entity_description.api_param
await self._async_update_hvac_params({param: False})

View File

@ -17,6 +17,7 @@ PLATFORMS: list[Platform] = [
Platform.CLIMATE, Platform.CLIMATE,
Platform.SELECT, Platform.SELECT,
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH,
Platform.WATER_HEATER, Platform.WATER_HEATER,
] ]

View File

@ -224,14 +224,20 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity):
self._attr_hvac_mode = HVACMode.OFF self._attr_hvac_mode = HVACMode.OFF
self._attr_max_temp = self.get_airzone_value(AZD_TEMP_SET_MAX) self._attr_max_temp = self.get_airzone_value(AZD_TEMP_SET_MAX)
self._attr_min_temp = self.get_airzone_value(AZD_TEMP_SET_MIN) self._attr_min_temp = self.get_airzone_value(AZD_TEMP_SET_MIN)
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: if (
self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
and self._attr_hvac_mode == HVACMode.HEAT_COOL
):
self._attr_target_temperature_high = self.get_airzone_value( self._attr_target_temperature_high = self.get_airzone_value(
AZD_TEMP_SET_COOL_AIR AZD_TEMP_SET_COOL_AIR
) )
self._attr_target_temperature_low = self.get_airzone_value( self._attr_target_temperature_low = self.get_airzone_value(
AZD_TEMP_SET_HOT_AIR AZD_TEMP_SET_HOT_AIR
) )
self._attr_target_temperature = None
else: else:
self._attr_target_temperature_high = None
self._attr_target_temperature_low = None
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)
@ -304,6 +310,10 @@ class AirzoneDeviceClimate(AirzoneClimate):
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
if hvac_mode is not None:
await self.async_set_hvac_mode(hvac_mode)
params: dict[str, Any] = {} params: dict[str, Any] = {}
if ATTR_TEMPERATURE in kwargs: if ATTR_TEMPERATURE in kwargs:
params[API_SETPOINT] = { params[API_SETPOINT] = {
@ -327,9 +337,6 @@ class AirzoneDeviceClimate(AirzoneClimate):
} }
await self._async_update_params(params) await self._async_update_params(params)
if ATTR_HVAC_MODE in kwargs:
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
class AirzoneDeviceGroupClimate(AirzoneClimate): class AirzoneDeviceGroupClimate(AirzoneClimate):
"""Define an Airzone Cloud DeviceGroup base class.""" """Define an Airzone Cloud DeviceGroup base class."""
@ -360,6 +367,10 @@ class AirzoneDeviceGroupClimate(AirzoneClimate):
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
if hvac_mode is not None:
await self.async_set_hvac_mode(hvac_mode)
params: dict[str, Any] = {} params: dict[str, Any] = {}
if ATTR_TEMPERATURE in kwargs: if ATTR_TEMPERATURE in kwargs:
params[API_PARAMS] = { params[API_PARAMS] = {
@ -370,9 +381,6 @@ class AirzoneDeviceGroupClimate(AirzoneClimate):
} }
await self._async_update_params(params) await self._async_update_params(params)
if ATTR_HVAC_MODE in kwargs:
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set hvac mode.""" """Set hvac mode."""
params: dict[str, Any] = { params: dict[str, Any] = {

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"], "loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.6.6"] "requirements": ["aioairzone-cloud==0.6.10"]
} }

View File

@ -2,14 +2,19 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Final from typing import Any, Final
from aioairzone_cloud.common import AirQualityMode from aioairzone_cloud.common import AirQualityMode, OperationMode
from aioairzone_cloud.const import ( from aioairzone_cloud.const import (
API_AQ_MODE_CONF, API_AQ_MODE_CONF,
API_MODE,
API_VALUE, API_VALUE,
AZD_AQ_MODE_CONF, AZD_AQ_MODE_CONF,
AZD_MASTER,
AZD_MODE,
AZD_MODES,
AZD_ZONES, AZD_ZONES,
) )
@ -28,7 +33,10 @@ class AirzoneSelectDescription(SelectEntityDescription):
"""Class to describe an Airzone select entity.""" """Class to describe an Airzone select entity."""
api_param: str api_param: str
options_dict: dict[str, str] options_dict: dict[str, Any]
options_fn: Callable[[dict[str, Any], dict[str, Any]], list[str]] = (
lambda zone_data, value: list(value)
)
AIR_QUALITY_MAP: Final[dict[str, str]] = { AIR_QUALITY_MAP: Final[dict[str, str]] = {
@ -37,6 +45,35 @@ AIR_QUALITY_MAP: Final[dict[str, str]] = {
"auto": AirQualityMode.AUTO, "auto": AirQualityMode.AUTO,
} }
MODE_MAP: Final[dict[str, int]] = {
"cool": OperationMode.COOLING,
"dry": OperationMode.DRY,
"fan": OperationMode.VENTILATION,
"heat": OperationMode.HEATING,
"heat_cool": OperationMode.AUTO,
"stop": OperationMode.STOP,
}
def main_zone_options(
zone_data: dict[str, Any],
options: dict[str, int],
) -> list[str]:
"""Filter available modes."""
modes = zone_data.get(AZD_MODES, [])
return [k for k, v in options.items() if v in modes]
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
AirzoneSelectDescription(
api_param=API_MODE,
key=AZD_MODE,
options_dict=MODE_MAP,
options_fn=main_zone_options,
translation_key="modes",
),
)
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
AirzoneSelectDescription( AirzoneSelectDescription(
@ -59,7 +96,19 @@ async def async_setup_entry(
coordinator = entry.runtime_data coordinator = entry.runtime_data
# Zones # Zones
async_add_entities( entities: list[AirzoneZoneSelect] = [
AirzoneZoneSelect(
coordinator,
description,
zone_id,
zone_data,
)
for description in MAIN_ZONE_SELECT_TYPES
for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items()
if description.key in zone_data and zone_data.get(AZD_MASTER)
]
entities.extend(
AirzoneZoneSelect( AirzoneZoneSelect(
coordinator, coordinator,
description, description,
@ -71,6 +120,8 @@ async def async_setup_entry(
if description.key in zone_data if description.key in zone_data
) )
async_add_entities(entities)
class AirzoneBaseSelect(AirzoneEntity, SelectEntity): class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
"""Define an Airzone Cloud select.""" """Define an Airzone Cloud select."""
@ -110,6 +161,11 @@ class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
self._attr_unique_id = f"{zone_id}_{description.key}" self._attr_unique_id = f"{zone_id}_{description.key}"
self.entity_description = description self.entity_description = description
self._attr_options = self.entity_description.options_fn(
zone_data, description.options_dict
)
self.values_dict = {v: k for k, v in description.options_dict.items()} self.values_dict = {v: k for k, v in description.options_dict.items()}
self._async_update_attrs() self._async_update_attrs()

View File

@ -36,6 +36,17 @@
"on": "On", "on": "On",
"auto": "Auto" "auto": "Auto"
} }
},
"modes": {
"name": "Mode",
"state": {
"cool": "[%key:component::climate::entity_component::_::state::cool%]",
"dry": "[%key:component::climate::entity_component::_::state::dry%]",
"fan": "[%key:component::climate::entity_component::_::state::fan_only%]",
"heat": "[%key:component::climate::entity_component::_::state::heat%]",
"heat_cool": "[%key:component::climate::entity_component::_::state::heat_cool%]",
"stop": "Stop"
}
} }
}, },
"sensor": { "sensor": {

View File

@ -0,0 +1,115 @@
"""Support for the Airzone Cloud switch."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Final
from aioairzone_cloud.const import API_POWER, API_VALUE, AZD_POWER, AZD_ZONES
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirzoneCloudConfigEntry
from .coordinator import AirzoneUpdateCoordinator
from .entity import AirzoneEntity, AirzoneZoneEntity
@dataclass(frozen=True, kw_only=True)
class AirzoneSwitchDescription(SwitchEntityDescription):
"""Class to describe an Airzone switch entity."""
api_param: str
ZONE_SWITCH_TYPES: Final[tuple[AirzoneSwitchDescription, ...]] = (
AirzoneSwitchDescription(
api_param=API_POWER,
device_class=SwitchDeviceClass.SWITCH,
key=AZD_POWER,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AirzoneCloudConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Airzone Cloud switch from a config_entry."""
coordinator = entry.runtime_data
# Zones
async_add_entities(
AirzoneZoneSwitch(
coordinator,
description,
zone_id,
zone_data,
)
for description in ZONE_SWITCH_TYPES
for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items()
if description.key in zone_data
)
class AirzoneBaseSwitch(AirzoneEntity, SwitchEntity):
"""Define an Airzone Cloud switch."""
entity_description: AirzoneSwitchDescription
@callback
def _handle_coordinator_update(self) -> None:
"""Update attributes when the coordinator updates."""
self._async_update_attrs()
super()._handle_coordinator_update()
@callback
def _async_update_attrs(self) -> None:
"""Update switch attributes."""
self._attr_is_on = self.get_airzone_value(self.entity_description.key)
class AirzoneZoneSwitch(AirzoneZoneEntity, AirzoneBaseSwitch):
"""Define an Airzone Cloud Zone switch."""
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
description: AirzoneSwitchDescription,
zone_id: str,
zone_data: dict[str, Any],
) -> None:
"""Initialize."""
super().__init__(coordinator, zone_id, zone_data)
self._attr_name = None
self._attr_unique_id = f"{zone_id}_{description.key}"
self.entity_description = description
self._async_update_attrs()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
param = self.entity_description.api_param
params: dict[str, Any] = {
param: {
API_VALUE: True,
}
}
await self._async_update_params(params)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
param = self.entity_description.api_param
params: dict[str, Any] = {
param: {
API_VALUE: False,
}
}
await self._async_update_params(params)

View File

@ -2,11 +2,13 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from datetime import timedelta from datetime import timedelta
from functools import cached_property, partial from functools import partial
import logging import logging
from typing import Any, Final, final from typing import Any, Final, final
from propcache import cached_property
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -32,6 +34,7 @@ from homeassistant.helpers.deprecation import (
) )
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
@ -48,6 +51,7 @@ from .const import ( # noqa: F401
ATTR_CODE_ARM_REQUIRED, ATTR_CODE_ARM_REQUIRED,
DOMAIN, DOMAIN,
AlarmControlPanelEntityFeature, AlarmControlPanelEntityFeature,
AlarmControlPanelState,
CodeFormat, CodeFormat,
) )
@ -141,6 +145,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
"changed_by", "changed_by",
"code_arm_required", "code_arm_required",
"supported_features", "supported_features",
"alarm_state",
} }
@ -148,6 +153,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
"""An abstract class for alarm control entities.""" """An abstract class for alarm control entities."""
entity_description: AlarmControlPanelEntityDescription entity_description: AlarmControlPanelEntityDescription
_attr_alarm_state: AlarmControlPanelState | None = None
_attr_changed_by: str | None = None _attr_changed_by: str | None = None
_attr_code_arm_required: bool = True _attr_code_arm_required: bool = True
_attr_code_format: CodeFormat | None = None _attr_code_format: CodeFormat | None = None
@ -156,6 +162,78 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
) )
_alarm_control_panel_option_default_code: str | None = None _alarm_control_panel_option_default_code: str | None = None
__alarm_legacy_state: bool = False
__alarm_legacy_state_reported: bool = False
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Post initialisation processing."""
super().__init_subclass__(**kwargs)
if any(method in cls.__dict__ for method in ("_attr_state", "state")):
# Integrations should use the 'alarm_state' property instead of
# setting the state directly.
cls.__alarm_legacy_state = True
def __setattr__(self, __name: str, __value: Any) -> None:
"""Set attribute.
Deprecation warning if setting '_attr_state' directly
unless already reported.
"""
if __name == "_attr_state":
if self.__alarm_legacy_state_reported is not True:
self._report_deprecated_alarm_state_handling()
self.__alarm_legacy_state_reported = True
return super().__setattr__(__name, __value)
@callback
def add_to_platform_start(
self,
hass: HomeAssistant,
platform: EntityPlatform,
parallel_updates: asyncio.Semaphore | None,
) -> None:
"""Start adding an entity to a platform."""
super().add_to_platform_start(hass, platform, parallel_updates)
if self.__alarm_legacy_state and not self.__alarm_legacy_state_reported:
self._report_deprecated_alarm_state_handling()
@callback
def _report_deprecated_alarm_state_handling(self) -> None:
"""Report on deprecated handling of alarm state.
Integrations should implement alarm_state instead of using state directly.
"""
self.__alarm_legacy_state_reported = True
if "custom_components" in type(self).__module__:
# Do not report on core integrations as they have been fixed.
report_issue = "report it to the custom integration author."
_LOGGER.warning(
"Entity %s (%s) is setting state directly"
" which will stop working in HA Core 2025.11."
" Entities should implement the 'alarm_state' property and"
" return its state using the AlarmControlPanelState enum, please %s",
self.entity_id,
type(self),
report_issue,
)
@final
@property
def state(self) -> str | None:
"""Return the current state."""
if (alarm_state := self.alarm_state) is None:
return None
return alarm_state
@cached_property
def alarm_state(self) -> AlarmControlPanelState | None:
"""Return the current alarm control panel entity state.
Integrations should overwrite this or use the '_attr_alarm_state'
attribute to set the alarm status using the 'AlarmControlPanelState' enum.
"""
return self._attr_alarm_state
@final @final
@callback @callback
def code_or_default_code(self, code: str | None) -> str | None: def code_or_default_code(self, code: str | None) -> str | None:

View File

@ -17,6 +17,21 @@ ATTR_CHANGED_BY: Final = "changed_by"
ATTR_CODE_ARM_REQUIRED: Final = "code_arm_required" ATTR_CODE_ARM_REQUIRED: Final = "code_arm_required"
class AlarmControlPanelState(StrEnum):
"""Alarm control panel entity states."""
DISARMED = "disarmed"
ARMED_HOME = "armed_home"
ARMED_AWAY = "armed_away"
ARMED_NIGHT = "armed_night"
ARMED_VACATION = "armed_vacation"
ARMED_CUSTOM_BYPASS = "armed_custom_bypass"
PENDING = "pending"
ARMING = "arming"
DISARMING = "disarming"
TRIGGERED = "triggered"
class CodeFormat(StrEnum): class CodeFormat(StrEnum):
"""Code formats for the Alarm Control Panel.""" """Code formats for the Alarm Control Panel."""

View File

@ -13,13 +13,6 @@ from homeassistant.const import (
CONF_DOMAIN, CONF_DOMAIN,
CONF_ENTITY_ID, CONF_ENTITY_ID,
CONF_TYPE, CONF_TYPE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_VACATION,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import ( from homeassistant.helpers import (
@ -31,7 +24,7 @@ from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
from homeassistant.helpers.entity import get_supported_features from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN from . import DOMAIN, AlarmControlPanelState
from .const import ( from .const import (
CONDITION_ARMED_AWAY, CONDITION_ARMED_AWAY,
CONDITION_ARMED_CUSTOM_BYPASS, CONDITION_ARMED_CUSTOM_BYPASS,
@ -109,19 +102,19 @@ def async_condition_from_config(
) -> condition.ConditionCheckerType: ) -> condition.ConditionCheckerType:
"""Create a function to test a device condition.""" """Create a function to test a device condition."""
if config[CONF_TYPE] == CONDITION_TRIGGERED: if config[CONF_TYPE] == CONDITION_TRIGGERED:
state = STATE_ALARM_TRIGGERED state = AlarmControlPanelState.TRIGGERED
elif config[CONF_TYPE] == CONDITION_DISARMED: elif config[CONF_TYPE] == CONDITION_DISARMED:
state = STATE_ALARM_DISARMED state = AlarmControlPanelState.DISARMED
elif config[CONF_TYPE] == CONDITION_ARMED_HOME: elif config[CONF_TYPE] == CONDITION_ARMED_HOME:
state = STATE_ALARM_ARMED_HOME state = AlarmControlPanelState.ARMED_HOME
elif config[CONF_TYPE] == CONDITION_ARMED_AWAY: elif config[CONF_TYPE] == CONDITION_ARMED_AWAY:
state = STATE_ALARM_ARMED_AWAY state = AlarmControlPanelState.ARMED_AWAY
elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT: elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT:
state = STATE_ALARM_ARMED_NIGHT state = AlarmControlPanelState.ARMED_NIGHT
elif config[CONF_TYPE] == CONDITION_ARMED_VACATION: elif config[CONF_TYPE] == CONDITION_ARMED_VACATION:
state = STATE_ALARM_ARMED_VACATION state = AlarmControlPanelState.ARMED_VACATION
elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS: elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS:
state = STATE_ALARM_ARMED_CUSTOM_BYPASS state = AlarmControlPanelState.ARMED_CUSTOM_BYPASS
registry = er.async_get(hass) registry = er.async_get(hass)
entity_id = er.async_resolve_entity_id(registry, config[ATTR_ENTITY_ID]) entity_id = er.async_resolve_entity_id(registry, config[ATTR_ENTITY_ID])

View File

@ -15,13 +15,6 @@ from homeassistant.const import (
CONF_FOR, CONF_FOR,
CONF_PLATFORM, CONF_PLATFORM,
CONF_TYPE, CONF_TYPE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_VACATION,
STATE_ALARM_ARMING,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
) )
from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers import config_validation as cv, entity_registry as er
@ -29,7 +22,7 @@ from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import DOMAIN from . import DOMAIN, AlarmControlPanelState
from .const import AlarmControlPanelEntityFeature from .const import AlarmControlPanelEntityFeature
BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"} BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"}
@ -129,19 +122,19 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE: ) -> CALLBACK_TYPE:
"""Attach a trigger.""" """Attach a trigger."""
if config[CONF_TYPE] == "triggered": if config[CONF_TYPE] == "triggered":
to_state = STATE_ALARM_TRIGGERED to_state = AlarmControlPanelState.TRIGGERED
elif config[CONF_TYPE] == "disarmed": elif config[CONF_TYPE] == "disarmed":
to_state = STATE_ALARM_DISARMED to_state = AlarmControlPanelState.DISARMED
elif config[CONF_TYPE] == "arming": elif config[CONF_TYPE] == "arming":
to_state = STATE_ALARM_ARMING to_state = AlarmControlPanelState.ARMING
elif config[CONF_TYPE] == "armed_home": elif config[CONF_TYPE] == "armed_home":
to_state = STATE_ALARM_ARMED_HOME to_state = AlarmControlPanelState.ARMED_HOME
elif config[CONF_TYPE] == "armed_away": elif config[CONF_TYPE] == "armed_away":
to_state = STATE_ALARM_ARMED_AWAY to_state = AlarmControlPanelState.ARMED_AWAY
elif config[CONF_TYPE] == "armed_night": elif config[CONF_TYPE] == "armed_night":
to_state = STATE_ALARM_ARMED_NIGHT to_state = AlarmControlPanelState.ARMED_NIGHT
elif config[CONF_TYPE] == "armed_vacation": elif config[CONF_TYPE] == "armed_vacation":
to_state = STATE_ALARM_ARMED_VACATION to_state = AlarmControlPanelState.ARMED_VACATION
state_config = { state_config = {
state_trigger.CONF_PLATFORM: "state", state_trigger.CONF_PLATFORM: "state",

View File

@ -16,28 +16,21 @@ from homeassistant.const import (
SERVICE_ALARM_ARM_VACATION, SERVICE_ALARM_ARM_VACATION,
SERVICE_ALARM_DISARM, SERVICE_ALARM_DISARM,
SERVICE_ALARM_TRIGGER, SERVICE_ALARM_TRIGGER,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_VACATION,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
) )
from homeassistant.core import Context, HomeAssistant, State from homeassistant.core import Context, HomeAssistant, State
from . import DOMAIN from . import DOMAIN, AlarmControlPanelState
_LOGGER: Final = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
VALID_STATES: Final[set[str]] = { VALID_STATES: Final[set[str]] = {
STATE_ALARM_ARMED_AWAY, AlarmControlPanelState.ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS, AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME, AlarmControlPanelState.ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, AlarmControlPanelState.ARMED_NIGHT,
STATE_ALARM_ARMED_VACATION, AlarmControlPanelState.ARMED_VACATION,
STATE_ALARM_DISARMED, AlarmControlPanelState.DISARMED,
STATE_ALARM_TRIGGERED, AlarmControlPanelState.TRIGGERED,
} }
@ -65,19 +58,19 @@ async def _async_reproduce_state(
service_data = {ATTR_ENTITY_ID: state.entity_id} service_data = {ATTR_ENTITY_ID: state.entity_id}
if state.state == STATE_ALARM_ARMED_AWAY: if state.state == AlarmControlPanelState.ARMED_AWAY:
service = SERVICE_ALARM_ARM_AWAY service = SERVICE_ALARM_ARM_AWAY
elif state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS: elif state.state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS:
service = SERVICE_ALARM_ARM_CUSTOM_BYPASS service = SERVICE_ALARM_ARM_CUSTOM_BYPASS
elif state.state == STATE_ALARM_ARMED_HOME: elif state.state == AlarmControlPanelState.ARMED_HOME:
service = SERVICE_ALARM_ARM_HOME service = SERVICE_ALARM_ARM_HOME
elif state.state == STATE_ALARM_ARMED_NIGHT: elif state.state == AlarmControlPanelState.ARMED_NIGHT:
service = SERVICE_ALARM_ARM_NIGHT service = SERVICE_ALARM_ARM_NIGHT
elif state.state == STATE_ALARM_ARMED_VACATION: elif state.state == AlarmControlPanelState.ARMED_VACATION:
service = SERVICE_ALARM_ARM_VACATION service = SERVICE_ALARM_ARM_VACATION
elif state.state == STATE_ALARM_DISARMED: elif state.state == AlarmControlPanelState.DISARMED:
service = SERVICE_ALARM_DISARM service = SERVICE_ALARM_DISARM
elif state.state == STATE_ALARM_TRIGGERED: elif state.state == AlarmControlPanelState.TRIGGERED:
service = SERVICE_ALARM_TRIGGER service = SERVICE_ALARM_TRIGGER
await hass.services.async_call( await hass.services.async_call(

View File

@ -7,16 +7,10 @@ import voluptuous as vol
from homeassistant.components.alarm_control_panel import ( from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity, AlarmControlPanelEntity,
AlarmControlPanelEntityFeature, AlarmControlPanelEntityFeature,
AlarmControlPanelState,
CodeFormat, CodeFormat,
) )
from homeassistant.const import ( from homeassistant.const import ATTR_CODE
ATTR_CODE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -106,15 +100,15 @@ class AlarmDecoderAlarmPanel(AlarmDecoderEntity, AlarmControlPanelEntity):
def _message_callback(self, message): def _message_callback(self, message):
"""Handle received messages.""" """Handle received messages."""
if message.alarm_sounding or message.fire_alarm: if message.alarm_sounding or message.fire_alarm:
self._attr_state = STATE_ALARM_TRIGGERED self._attr_alarm_state = AlarmControlPanelState.TRIGGERED
elif message.armed_away: elif message.armed_away:
self._attr_state = STATE_ALARM_ARMED_AWAY self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
elif message.armed_home and (message.entry_delay_off or message.perimeter_only): elif message.armed_home and (message.entry_delay_off or message.perimeter_only):
self._attr_state = STATE_ALARM_ARMED_NIGHT self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
elif message.armed_home: elif message.armed_home:
self._attr_state = STATE_ALARM_ARMED_HOME self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
else: else:
self._attr_state = STATE_ALARM_DISARMED self._attr_alarm_state = AlarmControlPanelState.DISARMED
self._attr_extra_state_attributes = { self._attr_extra_state_attributes = {
"ac_power": message.ac_power, "ac_power": message.ac_power,

View File

@ -157,7 +157,7 @@ class AlarmDecoderFlowHandler(ConfigFlow, domain=DOMAIN):
class AlarmDecoderOptionsFlowHandler(OptionsFlow): class AlarmDecoderOptionsFlowHandler(OptionsFlow):
"""Handle AlarmDecoder options.""" """Handle AlarmDecoder options."""
selected_zone: str | None = None selected_zone: str
def __init__(self, config_entry: ConfigEntry) -> None: def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize AlarmDecoder options flow.""" """Initialize AlarmDecoder options flow."""

View File

@ -26,6 +26,7 @@ from homeassistant.components import (
) )
from homeassistant.components.alarm_control_panel import ( from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature, AlarmControlPanelEntityFeature,
AlarmControlPanelState,
CodeFormat, CodeFormat,
) )
from homeassistant.components.climate import HVACMode from homeassistant.components.climate import HVACMode
@ -36,10 +37,6 @@ from homeassistant.const import (
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE, PERCENTAGE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_IDLE, STATE_IDLE,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
@ -1317,13 +1314,13 @@ class AlexaSecurityPanelController(AlexaCapability):
raise UnsupportedProperty(name) raise UnsupportedProperty(name)
arm_state = self.entity.state arm_state = self.entity.state
if arm_state == STATE_ALARM_ARMED_HOME: if arm_state == AlarmControlPanelState.ARMED_HOME:
return "ARMED_STAY" return "ARMED_STAY"
if arm_state == STATE_ALARM_ARMED_AWAY: if arm_state == AlarmControlPanelState.ARMED_AWAY:
return "ARMED_AWAY" return "ARMED_AWAY"
if arm_state == STATE_ALARM_ARMED_NIGHT: if arm_state == AlarmControlPanelState.ARMED_NIGHT:
return "ARMED_NIGHT" return "ARMED_NIGHT"
if arm_state == STATE_ALARM_ARMED_CUSTOM_BYPASS: if arm_state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS:
return "ARMED_STAY" return "ARMED_STAY"
return "DISARMED" return "DISARMED"

View File

@ -9,6 +9,7 @@ from typing import Any
from homeassistant import core as ha from homeassistant import core as ha
from homeassistant.components import ( from homeassistant.components import (
alarm_control_panel,
button, button,
camera, camera,
climate, climate,
@ -51,7 +52,6 @@ from homeassistant.const import (
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET, SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP, SERVICE_VOLUME_UP,
STATE_ALARM_DISARMED,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.helpers import network from homeassistant.helpers import network
@ -1083,7 +1083,13 @@ async def async_api_arm(
arm_state = directive.payload["armState"] arm_state = directive.payload["armState"]
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
if entity.state != STATE_ALARM_DISARMED: # Per Alexa Documentation: users are not allowed to switch from armed_away
# directly to another armed state without first disarming the system.
# https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-securitypanelcontroller.html#arming
if (
entity.state == alarm_control_panel.AlarmControlPanelState.ARMED_AWAY
and arm_state != "ARMED_AWAY"
):
msg = "You must disarm the system before you can set the requested arm state." msg = "You must disarm the system before you can set the requested arm state."
raise AlexaSecurityPanelAuthorizationRequired(msg) raise AlexaSecurityPanelAuthorizationRequired(msg)
@ -1133,7 +1139,7 @@ async def async_api_disarm(
# Per Alexa Documentation: If you receive a Disarm directive, and the # Per Alexa Documentation: If you receive a Disarm directive, and the
# system is already disarmed, respond with a success response, # system is already disarmed, respond with a success response,
# not an error response. # not an error response.
if entity.state == STATE_ALARM_DISARMED: if entity.state == alarm_control_panel.AlarmControlPanelState.DISARMED:
return response return response
payload = directive.payload payload = directive.payload

View File

@ -10,12 +10,15 @@ from homeassistant.core import Event, HassJob, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.event import async_call_later, async_track_time_interval
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from .analytics import Analytics from .analytics import Analytics
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
"""Set up the analytics integration.""" """Set up the analytics integration."""
@ -52,7 +55,7 @@ async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
websocket_api.async_register_command(hass, websocket_analytics) websocket_api.async_register_command(hass, websocket_analytics)
websocket_api.async_register_command(hass, websocket_analytics_preferences) websocket_api.async_register_command(hass, websocket_analytics_preferences)
hass.data[DOMAIN] = analytics hass.data[DATA_COMPONENT] = analytics
return True return True
@ -65,7 +68,7 @@ def websocket_analytics(
msg: dict[str, Any], msg: dict[str, Any],
) -> None: ) -> None:
"""Return analytics preferences.""" """Return analytics preferences."""
analytics: Analytics = hass.data[DOMAIN] analytics = hass.data[DATA_COMPONENT]
connection.send_result( connection.send_result(
msg["id"], msg["id"],
{ATTR_PREFERENCES: analytics.preferences, ATTR_ONBOARDED: analytics.onboarded}, {ATTR_PREFERENCES: analytics.preferences, ATTR_ONBOARDED: analytics.onboarded},
@ -87,7 +90,7 @@ async def websocket_analytics_preferences(
) -> None: ) -> None:
"""Update analytics preferences.""" """Update analytics preferences."""
preferences = msg[ATTR_PREFERENCES] preferences = msg[ATTR_PREFERENCES]
analytics: Analytics = hass.data[DOMAIN] analytics = hass.data[DATA_COMPONENT]
await analytics.save_preferences(preferences) await analytics.save_preferences(preferences)
await analytics.send_analytics() await analytics.send_analytics()

View File

@ -29,6 +29,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.entity_registry as er import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.loader import ( from homeassistant.loader import (
@ -136,7 +137,7 @@ class Analytics:
@property @property
def supervisor(self) -> bool: def supervisor(self) -> bool:
"""Return bool if a supervisor is present.""" """Return bool if a supervisor is present."""
return hassio.is_hassio(self.hass) return is_hassio(self.hass)
async def load(self) -> None: async def load(self) -> None:
"""Load preferences.""" """Load preferences."""

View File

@ -1,7 +1,7 @@
{ {
"domain": "analytics", "domain": "analytics",
"name": "Analytics", "name": "Analytics",
"after_dependencies": ["energy", "recorder"], "after_dependencies": ["energy", "hassio", "recorder"],
"codeowners": ["@home-assistant/core", "@ludeeus"], "codeowners": ["@home-assistant/core", "@ludeeus"],
"dependencies": ["api", "websocket_api"], "dependencies": ["api", "websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/analytics", "documentation": "https://www.home-assistant.io/integrations/analytics",

View File

@ -27,6 +27,7 @@ from homeassistant.helpers.selector import (
) )
from .const import ( from .const import (
CONF_TRACKED_ADDONS,
CONF_TRACKED_CUSTOM_INTEGRATIONS, CONF_TRACKED_CUSTOM_INTEGRATIONS,
CONF_TRACKED_INTEGRATIONS, CONF_TRACKED_INTEGRATIONS,
DOMAIN, DOMAIN,
@ -55,8 +56,12 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step.""" """Handle the initial step."""
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get( if all(
CONF_TRACKED_CUSTOM_INTEGRATIONS [
not user_input.get(CONF_TRACKED_ADDONS),
not user_input.get(CONF_TRACKED_INTEGRATIONS),
not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
]
): ):
errors["base"] = "no_integrations_selected" errors["base"] = "no_integrations_selected"
else: else:
@ -64,6 +69,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
title="Home Assistant Analytics Insights", title="Home Assistant Analytics Insights",
data={}, data={},
options={ options={
CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
CONF_TRACKED_INTEGRATIONS: user_input.get( CONF_TRACKED_INTEGRATIONS: user_input.get(
CONF_TRACKED_INTEGRATIONS, [] CONF_TRACKED_INTEGRATIONS, []
), ),
@ -77,6 +83,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
session=async_get_clientsession(self.hass) session=async_get_clientsession(self.hass)
) )
try: try:
addons = await client.get_addons()
integrations = await client.get_integrations() integrations = await client.get_integrations()
custom_integrations = await client.get_custom_integrations() custom_integrations = await client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError: except HomeassistantAnalyticsConnectionError:
@ -99,6 +106,13 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
data_schema=vol.Schema( data_schema=vol.Schema(
{ {
vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
SelectSelectorConfig(
options=list(addons),
multiple=True,
sort=True,
)
),
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector( vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
SelectSelectorConfig( SelectSelectorConfig(
options=options, options=options,
@ -127,14 +141,19 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Manage the options.""" """Manage the options."""
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get( if all(
CONF_TRACKED_CUSTOM_INTEGRATIONS [
not user_input.get(CONF_TRACKED_ADDONS),
not user_input.get(CONF_TRACKED_INTEGRATIONS),
not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
]
): ):
errors["base"] = "no_integrations_selected" errors["base"] = "no_integrations_selected"
else: else:
return self.async_create_entry( return self.async_create_entry(
title="", title="",
data={ data={
CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
CONF_TRACKED_INTEGRATIONS: user_input.get( CONF_TRACKED_INTEGRATIONS: user_input.get(
CONF_TRACKED_INTEGRATIONS, [] CONF_TRACKED_INTEGRATIONS, []
), ),
@ -148,6 +167,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
session=async_get_clientsession(self.hass) session=async_get_clientsession(self.hass)
) )
try: try:
addons = await client.get_addons()
integrations = await client.get_integrations() integrations = await client.get_integrations()
custom_integrations = await client.get_custom_integrations() custom_integrations = await client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError: except HomeassistantAnalyticsConnectionError:
@ -168,6 +188,13 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
data_schema=self.add_suggested_values_to_schema( data_schema=self.add_suggested_values_to_schema(
vol.Schema( vol.Schema(
{ {
vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
SelectSelectorConfig(
options=list(addons),
multiple=True,
sort=True,
)
),
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector( vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
SelectSelectorConfig( SelectSelectorConfig(
options=options, options=options,

View File

@ -4,6 +4,7 @@ import logging
DOMAIN = "analytics_insights" DOMAIN = "analytics_insights"
CONF_TRACKED_ADDONS = "tracked_addons"
CONF_TRACKED_INTEGRATIONS = "tracked_integrations" CONF_TRACKED_INTEGRATIONS = "tracked_integrations"
CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations" CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations"

View File

@ -12,11 +12,13 @@ from python_homeassistant_analytics import (
HomeassistantAnalyticsConnectionError, HomeassistantAnalyticsConnectionError,
HomeassistantAnalyticsNotModifiedError, HomeassistantAnalyticsNotModifiedError,
) )
from python_homeassistant_analytics.models import Addon
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ( from .const import (
CONF_TRACKED_ADDONS,
CONF_TRACKED_CUSTOM_INTEGRATIONS, CONF_TRACKED_CUSTOM_INTEGRATIONS,
CONF_TRACKED_INTEGRATIONS, CONF_TRACKED_INTEGRATIONS,
DOMAIN, DOMAIN,
@ -31,6 +33,9 @@ if TYPE_CHECKING:
class AnalyticsData: class AnalyticsData:
"""Analytics data class.""" """Analytics data class."""
active_installations: int
reports_integrations: int
addons: dict[str, int]
core_integrations: dict[str, int] core_integrations: dict[str, int]
custom_integrations: dict[str, int] custom_integrations: dict[str, int]
@ -51,6 +56,7 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
update_interval=timedelta(hours=12), update_interval=timedelta(hours=12),
) )
self._client = client self._client = client
self._tracked_addons = self.config_entry.options.get(CONF_TRACKED_ADDONS, [])
self._tracked_integrations = self.config_entry.options[ self._tracked_integrations = self.config_entry.options[
CONF_TRACKED_INTEGRATIONS CONF_TRACKED_INTEGRATIONS
] ]
@ -60,6 +66,7 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
async def _async_update_data(self) -> AnalyticsData: async def _async_update_data(self) -> AnalyticsData:
try: try:
addons_data = await self._client.get_addons()
data = await self._client.get_current_analytics() data = await self._client.get_current_analytics()
custom_data = await self._client.get_custom_integrations() custom_data = await self._client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError as err: except HomeassistantAnalyticsConnectionError as err:
@ -68,6 +75,9 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
) from err ) from err
except HomeassistantAnalyticsNotModifiedError: except HomeassistantAnalyticsNotModifiedError:
return self.data return self.data
addons = {
addon: get_addon_value(addons_data, addon) for addon in self._tracked_addons
}
core_integrations = { core_integrations = {
integration: data.integrations.get(integration, 0) integration: data.integrations.get(integration, 0)
for integration in self._tracked_integrations for integration in self._tracked_integrations
@ -76,7 +86,20 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
integration: get_custom_integration_value(custom_data, integration) integration: get_custom_integration_value(custom_data, integration)
for integration in self._tracked_custom_integrations for integration in self._tracked_custom_integrations
} }
return AnalyticsData(core_integrations, custom_integrations) return AnalyticsData(
data.active_installations,
data.reports_integrations,
addons,
core_integrations,
custom_integrations,
)
def get_addon_value(data: dict[str, Addon], name_slug: str) -> int:
"""Get addon value."""
if name_slug in data:
return data[name_slug].total
return 0
def get_custom_integration_value( def get_custom_integration_value(

View File

@ -6,6 +6,12 @@
}, },
"custom_integrations": { "custom_integrations": {
"default": "mdi:puzzle-edit" "default": "mdi:puzzle-edit"
},
"total_active_installations": {
"default": "mdi:puzzle"
},
"total_reports_integrations": {
"default": "mdi:puzzle"
} }
} }
} }

View File

@ -7,6 +7,6 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["python_homeassistant_analytics"], "loggers": ["python_homeassistant_analytics"],
"requirements": ["python-homeassistant-analytics==0.7.0"], "requirements": ["python-homeassistant-analytics==0.8.0"],
"single_config_entry": true "single_config_entry": true
} }

View File

@ -29,6 +29,20 @@ class AnalyticsSensorEntityDescription(SensorEntityDescription):
value_fn: Callable[[AnalyticsData], StateType] value_fn: Callable[[AnalyticsData], StateType]
def get_addon_entity_description(
name_slug: str,
) -> AnalyticsSensorEntityDescription:
"""Get addon entity description."""
return AnalyticsSensorEntityDescription(
key=f"addon_{name_slug}_active_installations",
translation_key="addons",
name=name_slug,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement="active installations",
value_fn=lambda data: data.addons.get(name_slug),
)
def get_core_integration_entity_description( def get_core_integration_entity_description(
domain: str, name: str domain: str, name: str
) -> AnalyticsSensorEntityDescription: ) -> AnalyticsSensorEntityDescription:
@ -57,6 +71,26 @@ def get_custom_integration_entity_description(
) )
GENERAL_SENSORS = [
AnalyticsSensorEntityDescription(
key="total_active_installations",
translation_key="total_active_installations",
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement="active installations",
value_fn=lambda data: data.active_installations,
),
AnalyticsSensorEntityDescription(
key="total_reports_integrations",
translation_key="total_reports_integrations",
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement="active installations",
value_fn=lambda data: data.reports_integrations,
),
]
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AnalyticsInsightsConfigEntry, entry: AnalyticsInsightsConfigEntry,
@ -69,6 +103,13 @@ async def async_setup_entry(
analytics_data.coordinator analytics_data.coordinator
) )
entities: list[HomeassistantAnalyticsSensor] = [] entities: list[HomeassistantAnalyticsSensor] = []
entities.extend(
HomeassistantAnalyticsSensor(
coordinator,
get_addon_entity_description(addon_name_slug),
)
for addon_name_slug in coordinator.data.addons
)
entities.extend( entities.extend(
HomeassistantAnalyticsSensor( HomeassistantAnalyticsSensor(
coordinator, coordinator,
@ -85,6 +126,12 @@ async def async_setup_entry(
) )
for integration_domain in coordinator.data.custom_integrations for integration_domain in coordinator.data.custom_integrations
) )
entities.extend(
HomeassistantAnalyticsSensor(coordinator, entity_description)
for entity_description in GENERAL_SENSORS
)
async_add_entities(entities) async_add_entities(entities)

View File

@ -3,10 +3,12 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"tracked_addons": "Addons",
"tracked_integrations": "Integrations", "tracked_integrations": "Integrations",
"tracked_custom_integrations": "Custom integrations" "tracked_custom_integrations": "Custom integrations"
}, },
"data_description": { "data_description": {
"tracked_addons": "Select the addons you want to track",
"tracked_integrations": "Select the integrations you want to track", "tracked_integrations": "Select the integrations you want to track",
"tracked_custom_integrations": "Select the custom integrations you want to track" "tracked_custom_integrations": "Select the custom integrations you want to track"
} }
@ -24,10 +26,12 @@
"step": { "step": {
"init": { "init": {
"data": { "data": {
"tracked_addons": "[%key:component::analytics_insights::config::step::user::data::tracked_addons%]",
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]", "tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]",
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]" "tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]"
}, },
"data_description": { "data_description": {
"tracked_addons": "[%key:component::analytics_insights::config::step::user::data_description::tracked_addons%]",
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]", "tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]",
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]" "tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]"
} }
@ -44,6 +48,12 @@
"sensor": { "sensor": {
"custom_integrations": { "custom_integrations": {
"name": "{custom_integration_domain} (custom)" "name": "{custom_integration_domain} (custom)"
},
"total_active_installations": {
"name": "Total active installations"
},
"total_reports_integrations": {
"name": "Total reported integrations"
} }
} }
} }

View File

@ -4,7 +4,6 @@ from __future__ import annotations
from pydroid_ipcam import PyDroidIPCam from pydroid_ipcam import PyDroidIPCam
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_PASSWORD, CONF_PASSWORD,
@ -15,8 +14,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
from .coordinator import AndroidIPCamDataUpdateCoordinator
PLATFORMS: list[Platform] = [ PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
@ -26,7 +24,9 @@ PLATFORMS: list[Platform] = [
] ]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(
hass: HomeAssistant, entry: AndroidIPCamConfigEntry
) -> bool:
"""Set up Android IP Webcam from a config entry.""" """Set up Android IP Webcam from a config entry."""
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)
cam = PyDroidIPCam( cam = PyDroidIPCam(
@ -40,16 +40,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = AndroidIPCamDataUpdateCoordinator(hass, entry, cam) coordinator = AndroidIPCamDataUpdateCoordinator(hass, entry, cam)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(
hass: HomeAssistant, entry: AndroidIPCamConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -7,12 +7,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, MOTION_ACTIVE from .const import MOTION_ACTIVE
from .coordinator import AndroidIPCamDataUpdateCoordinator from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
from .entity import AndroidIPCamBaseEntity from .entity import AndroidIPCamBaseEntity
BINARY_SENSOR_DESCRIPTION = BinarySensorEntityDescription( BINARY_SENSOR_DESCRIPTION = BinarySensorEntityDescription(
@ -24,16 +23,12 @@ BINARY_SENSOR_DESCRIPTION = BinarySensorEntityDescription(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AndroidIPCamConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the IP Webcam sensors from config entry.""" """Set up the IP Webcam sensors from config entry."""
coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][ async_add_entities([IPWebcamBinarySensor(config_entry.runtime_data)])
config_entry.entry_id
]
async_add_entities([IPWebcamBinarySensor(coordinator)])
class IPWebcamBinarySensor(AndroidIPCamBaseEntity, BinarySensorEntity): class IPWebcamBinarySensor(AndroidIPCamBaseEntity, BinarySensorEntity):

View File

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_PASSWORD, CONF_PASSWORD,
@ -15,21 +14,17 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AndroidIPCamDataUpdateCoordinator from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AndroidIPCamConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the IP Webcam camera from config entry.""" """Set up the IP Webcam camera from config entry."""
filter_urllib3_logging() filter_urllib3_logging()
coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][ async_add_entities([IPWebcamCamera(config_entry.runtime_data)])
config_entry.entry_id
]
async_add_entities([IPWebcamCamera(coordinator)])
class IPWebcamCamera(MjpegCamera): class IPWebcamCamera(MjpegCamera):

View File

@ -15,19 +15,22 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type AndroidIPCamConfigEntry = ConfigEntry[AndroidIPCamDataUpdateCoordinator]
class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]): class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""Coordinator class for the Android IP Webcam.""" """Coordinator class for the Android IP Webcam."""
config_entry: AndroidIPCamConfigEntry
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AndroidIPCamConfigEntry,
cam: PyDroidIPCam, cam: PyDroidIPCam,
) -> None: ) -> None:
"""Initialize the Android IP Webcam.""" """Initialize the Android IP Webcam."""
self.hass = hass self.hass = hass
self.config_entry: ConfigEntry = config_entry
self.cam = cam self.cam = cam
super().__init__( super().__init__(
self.hass, self.hass,

View File

@ -13,14 +13,12 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from .const import DOMAIN from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
from .coordinator import AndroidIPCamDataUpdateCoordinator
from .entity import AndroidIPCamBaseEntity from .entity import AndroidIPCamBaseEntity
@ -120,19 +118,21 @@ SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AndroidIPCamConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the IP Webcam sensors from config entry.""" """Set up the IP Webcam sensors from config entry."""
coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][ coordinator = config_entry.runtime_data
config_entry.entry_id
]
sensor_types = [ sensor_types = [
sensor sensor
for sensor in SENSOR_TYPES for sensor in SENSOR_TYPES
if sensor.key if sensor.key
in [*coordinator.cam.enabled_sensors, "audio_connections", "video_connections"] in [
*coordinator.cam.enabled_sensors,
"audio_connections",
"video_connections",
]
] ]
async_add_entities( async_add_entities(
IPWebcamSensor(coordinator, description) for description in sensor_types IPWebcamSensor(coordinator, description) for description in sensor_types

View File

@ -9,13 +9,11 @@ from typing import Any
from pydroid_ipcam import PyDroidIPCam from pydroid_ipcam import PyDroidIPCam
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
from .coordinator import AndroidIPCamDataUpdateCoordinator
from .entity import AndroidIPCamBaseEntity from .entity import AndroidIPCamBaseEntity
@ -113,14 +111,12 @@ SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AndroidIPCamConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the IP Webcam switches from config entry.""" """Set up the IP Webcam switches from config entry."""
coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][ coordinator = config_entry.runtime_data
config_entry.entry_id
]
switch_types = [ switch_types = [
switch switch
for switch in SWITCH_TYPES for switch in SWITCH_TYPES

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import dataclass from dataclasses import dataclass
import logging
import os import os
from typing import Any from typing import Any
@ -40,6 +41,7 @@ from .const import (
CONF_ADB_SERVER_IP, CONF_ADB_SERVER_IP,
CONF_ADB_SERVER_PORT, CONF_ADB_SERVER_PORT,
CONF_ADBKEY, CONF_ADBKEY,
CONF_SCREENCAP_INTERVAL,
CONF_STATE_DETECTION_RULES, CONF_STATE_DETECTION_RULES,
DEFAULT_ADB_SERVER_PORT, DEFAULT_ADB_SERVER_PORT,
DEVICE_ANDROIDTV, DEVICE_ANDROIDTV,
@ -66,6 +68,8 @@ RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"} _INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
_LOGGER = logging.getLogger(__name__)
@dataclass @dataclass
class AndroidTVRuntimeData: class AndroidTVRuntimeData:
@ -157,6 +161,32 @@ async def async_connect_androidtv(
return aftv, None return aftv, None
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating configuration from version %s.%s", entry.version, entry.minor_version
)
if entry.version == 1:
new_options = {**entry.options}
# Migrate MinorVersion 1 -> MinorVersion 2: New option
if entry.minor_version < 2:
new_options = {**new_options, CONF_SCREENCAP_INTERVAL: 0}
hass.config_entries.async_update_entry(
entry, options=new_options, minor_version=2, version=1
)
_LOGGER.debug(
"Migration to configuration version %s.%s successful",
entry.version,
entry.minor_version,
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool:
"""Set up Android Debug Bridge platform.""" """Set up Android Debug Bridge platform."""

View File

@ -34,7 +34,7 @@ from .const import (
CONF_APPS, CONF_APPS,
CONF_EXCLUDE_UNNAMED_APPS, CONF_EXCLUDE_UNNAMED_APPS,
CONF_GET_SOURCES, CONF_GET_SOURCES,
CONF_SCREENCAP, CONF_SCREENCAP_INTERVAL,
CONF_STATE_DETECTION_RULES, CONF_STATE_DETECTION_RULES,
CONF_TURN_OFF_COMMAND, CONF_TURN_OFF_COMMAND,
CONF_TURN_ON_COMMAND, CONF_TURN_ON_COMMAND,
@ -43,7 +43,7 @@ from .const import (
DEFAULT_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS,
DEFAULT_GET_SOURCES, DEFAULT_GET_SOURCES,
DEFAULT_PORT, DEFAULT_PORT,
DEFAULT_SCREENCAP, DEFAULT_SCREENCAP_INTERVAL,
DEVICE_CLASSES, DEVICE_CLASSES,
DOMAIN, DOMAIN,
PROP_ETHMAC, PROP_ETHMAC,
@ -76,6 +76,7 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow.""" """Handle a config flow."""
VERSION = 1 VERSION = 1
MINOR_VERSION = 2
@callback @callback
def _show_setup_form( def _show_setup_form(
@ -253,10 +254,12 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry):
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
), ),
): bool, ): bool,
vol.Optional( vol.Required(
CONF_SCREENCAP, CONF_SCREENCAP_INTERVAL,
default=options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP), default=options.get(
): bool, CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
),
): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=15)),
vol.Optional( vol.Optional(
CONF_TURN_OFF_COMMAND, CONF_TURN_OFF_COMMAND,
description={ description={

View File

@ -9,6 +9,7 @@ CONF_APPS = "apps"
CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps" CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps"
CONF_GET_SOURCES = "get_sources" CONF_GET_SOURCES = "get_sources"
CONF_SCREENCAP = "screencap" CONF_SCREENCAP = "screencap"
CONF_SCREENCAP_INTERVAL = "screencap_interval"
CONF_STATE_DETECTION_RULES = "state_detection_rules" CONF_STATE_DETECTION_RULES = "state_detection_rules"
CONF_TURN_OFF_COMMAND = "turn_off_command" CONF_TURN_OFF_COMMAND = "turn_off_command"
CONF_TURN_ON_COMMAND = "turn_on_command" CONF_TURN_ON_COMMAND = "turn_on_command"
@ -18,7 +19,7 @@ DEFAULT_DEVICE_CLASS = "auto"
DEFAULT_EXCLUDE_UNNAMED_APPS = False DEFAULT_EXCLUDE_UNNAMED_APPS = False
DEFAULT_GET_SOURCES = True DEFAULT_GET_SOURCES = True
DEFAULT_PORT = 5555 DEFAULT_PORT = 5555
DEFAULT_SCREENCAP = True DEFAULT_SCREENCAP_INTERVAL = 5
DEVICE_ANDROIDTV = "androidtv" DEVICE_ANDROIDTV = "androidtv"
DEVICE_FIRETV = "firetv" DEVICE_FIRETV = "firetv"

View File

@ -2,10 +2,9 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import datetime, timedelta
import hashlib import hashlib
import logging import logging
from typing import Any
from androidtv.constants import APPS, KEYS from androidtv.constants import APPS, KEYS
from androidtv.setup_async import AndroidTVAsync, FireTVAsync from androidtv.setup_async import AndroidTVAsync, FireTVAsync
@ -23,19 +22,19 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle from homeassistant.util.dt import utcnow
from . import AndroidTVConfigEntry from . import AndroidTVConfigEntry
from .const import ( from .const import (
CONF_APPS, CONF_APPS,
CONF_EXCLUDE_UNNAMED_APPS, CONF_EXCLUDE_UNNAMED_APPS,
CONF_GET_SOURCES, CONF_GET_SOURCES,
CONF_SCREENCAP, CONF_SCREENCAP_INTERVAL,
CONF_TURN_OFF_COMMAND, CONF_TURN_OFF_COMMAND,
CONF_TURN_ON_COMMAND, CONF_TURN_ON_COMMAND,
DEFAULT_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS,
DEFAULT_GET_SOURCES, DEFAULT_GET_SOURCES,
DEFAULT_SCREENCAP, DEFAULT_SCREENCAP_INTERVAL,
DEVICE_ANDROIDTV, DEVICE_ANDROIDTV,
SIGNAL_CONFIG_ENTITY, SIGNAL_CONFIG_ENTITY,
) )
@ -48,8 +47,6 @@ ATTR_DEVICE_PATH = "device_path"
ATTR_HDMI_INPUT = "hdmi_input" ATTR_HDMI_INPUT = "hdmi_input"
ATTR_LOCAL_PATH = "local_path" ATTR_LOCAL_PATH = "local_path"
MIN_TIME_BETWEEN_SCREENCAPS = timedelta(seconds=60)
SERVICE_ADB_COMMAND = "adb_command" SERVICE_ADB_COMMAND = "adb_command"
SERVICE_DOWNLOAD = "download" SERVICE_DOWNLOAD = "download"
SERVICE_LEARN_SENDEVENT = "learn_sendevent" SERVICE_LEARN_SENDEVENT = "learn_sendevent"
@ -125,7 +122,8 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
self._app_name_to_id: dict[str, str] = {} self._app_name_to_id: dict[str, str] = {}
self._get_sources = DEFAULT_GET_SOURCES self._get_sources = DEFAULT_GET_SOURCES
self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS
self._screencap = DEFAULT_SCREENCAP self._screencap_delta: timedelta | None = None
self._last_screencap: datetime | None = None
self.turn_on_command: str | None = None self.turn_on_command: str | None = None
self.turn_off_command: str | None = None self.turn_off_command: str | None = None
@ -159,7 +157,13 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
self._exclude_unnamed_apps = options.get( self._exclude_unnamed_apps = options.get(
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
) )
self._screencap = options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP) screencap_interval: int = options.get(
CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
)
if screencap_interval > 0:
self._screencap_delta = timedelta(minutes=screencap_interval)
else:
self._screencap_delta = None
self.turn_off_command = options.get(CONF_TURN_OFF_COMMAND) self.turn_off_command = options.get(CONF_TURN_OFF_COMMAND)
self.turn_on_command = options.get(CONF_TURN_ON_COMMAND) self.turn_on_command = options.get(CONF_TURN_ON_COMMAND)
@ -183,7 +187,7 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
async def _async_get_screencap(self, prev_app_id: str | None = None) -> None: async def _async_get_screencap(self, prev_app_id: str | None = None) -> None:
"""Take a screen capture from the device when enabled.""" """Take a screen capture from the device when enabled."""
if ( if (
not self._screencap not self._screencap_delta
or self.state in {MediaPlayerState.OFF, None} or self.state in {MediaPlayerState.OFF, None}
or not self.available or not self.available
): ):
@ -193,11 +197,18 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
force: bool = prev_app_id is not None force: bool = prev_app_id is not None
if force: if force:
force = prev_app_id != self._attr_app_id force = prev_app_id != self._attr_app_id
await self._adb_get_screencap(no_throttle=force) await self._adb_get_screencap(force)
@Throttle(MIN_TIME_BETWEEN_SCREENCAPS) async def _adb_get_screencap(self, force: bool = False) -> None:
async def _adb_get_screencap(self, **kwargs: Any) -> None: """Take a screen capture from the device every configured minutes."""
"""Take a screen capture from the device every 60 seconds.""" time_elapsed = self._screencap_delta is not None and (
self._last_screencap is None
or (utcnow() - self._last_screencap) >= self._screencap_delta
)
if not (force or time_elapsed):
return
self._last_screencap = utcnow()
if media_data := await self._adb_screencap(): if media_data := await self._adb_screencap():
self._media_image = media_data, "image/png" self._media_image = media_data, "image/png"
self._attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16] self._attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16]

View File

@ -31,7 +31,7 @@
"apps": "Configure applications list", "apps": "Configure applications list",
"get_sources": "Retrieve the running apps as the list of sources", "get_sources": "Retrieve the running apps as the list of sources",
"exclude_unnamed_apps": "Exclude apps with unknown name from the sources list", "exclude_unnamed_apps": "Exclude apps with unknown name from the sources list",
"screencap": "Use screen capture for album art", "screencap_interval": "Interval in minutes between screen capture for album art (set 0 to disable)",
"state_detection_rules": "Configure state detection rules", "state_detection_rules": "Configure state detection rules",
"turn_off_command": "ADB shell turn off command (leave empty for default)", "turn_off_command": "ADB shell turn off command (leave empty for default)",
"turn_on_command": "ADB shell turn on command (leave empty for default)" "turn_on_command": "ADB shell turn on command (leave empty for default)"

View File

@ -16,6 +16,7 @@ import voluptuous as vol
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.config_entries import ( from homeassistant.config_entries import (
SOURCE_REAUTH,
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
@ -58,13 +59,10 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
def __init__(self) -> None: api: AndroidTVRemote
"""Initialize a new AndroidTVRemoteConfigFlow.""" host: str
self.api: AndroidTVRemote | None = None name: str
self.reauth_entry: ConfigEntry | None = None mac: str
self.host: str | None = None
self.name: str | None = None
self.mac: str | None = None
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@ -72,13 +70,11 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step.""" """Handle the initial step."""
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
self.host = user_input["host"] self.host = user_input[CONF_HOST]
assert self.host
api = create_api(self.hass, self.host, enable_ime=False) api = create_api(self.hass, self.host, enable_ime=False)
try: try:
await api.async_generate_cert_if_missing() await api.async_generate_cert_if_missing()
self.name, self.mac = await api.async_get_name_and_mac() self.name, self.mac = await api.async_get_name_and_mac()
assert self.mac
await self.async_set_unique_id(format_mac(self.mac)) await self.async_set_unique_id(format_mac(self.mac))
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host}) self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
return await self._async_start_pair() return await self._async_start_pair()
@ -94,7 +90,6 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
async def _async_start_pair(self) -> ConfigFlowResult: async def _async_start_pair(self) -> ConfigFlowResult:
"""Start pairing with the Android TV. Navigate to the pair flow to enter the PIN shown on screen.""" """Start pairing with the Android TV. Navigate to the pair flow to enter the PIN shown on screen."""
assert self.host
self.api = create_api(self.hass, self.host, enable_ime=False) self.api = create_api(self.hass, self.host, enable_ime=False)
await self.api.async_generate_cert_if_missing() await self.api.async_generate_cert_if_missing()
await self.api.async_start_pairing() await self.api.async_start_pairing()
@ -108,14 +103,12 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
try: try:
pin = user_input["pin"] pin = user_input["pin"]
assert self.api
await self.api.async_finish_pairing(pin) await self.api.async_finish_pairing(pin)
if self.reauth_entry: if self.source == SOURCE_REAUTH:
await self.hass.config_entries.async_reload( await self.hass.config_entries.async_reload(
self.reauth_entry.entry_id self._get_reauth_entry().entry_id
) )
return self.async_abort(reason="reauth_successful") return self.async_abort(reason="reauth_successful")
assert self.name
return self.async_create_entry( return self.async_create_entry(
title=self.name, title=self.name,
data={ data={
@ -155,10 +148,21 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("Android TV device found via zeroconf: %s", discovery_info) _LOGGER.debug("Android TV device found via zeroconf: %s", discovery_info)
self.host = discovery_info.host self.host = discovery_info.host
self.name = discovery_info.name.removesuffix("._androidtvremote2._tcp.local.") self.name = discovery_info.name.removesuffix("._androidtvremote2._tcp.local.")
self.mac = discovery_info.properties.get("bt") if not (mac := discovery_info.properties.get("bt")):
if not self.mac:
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
await self.async_set_unique_id(format_mac(self.mac)) self.mac = mac
existing_config_entry = await self.async_set_unique_id(format_mac(mac))
# Sometimes, devices send an invalid zeroconf message with multiple addresses
# and one of them, which could end up being in discovery_info.host, is from a
# different device. If any of the discovery_info.ip_addresses matches the
# existing host, don't update the host.
if existing_config_entry and len(discovery_info.ip_addresses) > 1:
existing_host = existing_config_entry.data[CONF_HOST]
if existing_host != self.host:
if existing_host in [
str(ip_address) for ip_address in discovery_info.ip_addresses
]:
self.host = existing_host
self._abort_if_unique_id_configured( self._abort_if_unique_id_configured(
updates={CONF_HOST: self.host, CONF_NAME: self.name} updates={CONF_HOST: self.host, CONF_NAME: self.name}
) )
@ -189,9 +193,6 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
self.host = entry_data[CONF_HOST] self.host = entry_data[CONF_HOST]
self.name = entry_data[CONF_NAME] self.name = entry_data[CONF_NAME]
self.mac = entry_data[CONF_MAC] self.mac = entry_data[CONF_MAC]
self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm() return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm( async def async_step_reauth_confirm(

View File

@ -13,7 +13,7 @@ from anova_wifi import (
WebsocketFailure, WebsocketFailure,
) )
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
@ -71,3 +71,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: AnovaConfigEntry) -> bo
# Disconnect from WS # Disconnect from WS
await entry.runtime_data.api.disconnect_websocket() await entry.runtime_data.api.disconnect_websocket()
return unload_ok return unload_ok
async def async_migrate_entry(hass: HomeAssistant, entry: AnovaConfigEntry) -> bool:
"""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:
new_data.pop(CONF_DEVICES)
hass.config_entries.async_update_entry(entry, data=new_data, minor_version=2)
_LOGGER.debug(
"Migration to version %s:%s successful", entry.version, entry.minor_version
)
return True

View File

@ -6,7 +6,7 @@ from anova_wifi import AnovaApi, InvalidLogin
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
@ -16,6 +16,7 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
"""Sets up a config flow for Anova.""" """Sets up a config flow for Anova."""
VERSION = 1 VERSION = 1
MINOR_VERSION = 2
async def async_step_user( async def async_step_user(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None
@ -42,8 +43,6 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
data={ data={
CONF_USERNAME: user_input[CONF_USERNAME], CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD], CONF_PASSWORD: user_input[CONF_PASSWORD],
# this can be removed in a migration to 1.2 in 2024.11
CONF_DEVICES: [],
}, },
) )

View File

@ -87,10 +87,13 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
except anthropic.APIConnectionError: except anthropic.APIConnectionError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except anthropic.APIStatusError as e: except anthropic.APIStatusError as e:
if isinstance(e.body, dict): errors["base"] = "unknown"
errors["base"] = e.body.get("error", {}).get("type", "unknown") if (
else: isinstance(e.body, dict)
errors["base"] = "unknown" and (error := e.body.get("error"))
and error.get("type") == "authentication_error"
):
errors["base"] = "authentication_error"
except Exception: except Exception:
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"

View File

@ -23,7 +23,7 @@ class AOSmithConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
_reauth_email: str | None = None _reauth_email: str
async def _async_validate_credentials( async def _async_validate_credentials(
self, email: str, password: str self, email: str, password: str
@ -85,21 +85,16 @@ class AOSmithConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle user's reauth credentials.""" """Handle user's reauth credentials."""
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None and self._reauth_email is not None: if user_input:
email = self._reauth_email
password = user_input[CONF_PASSWORD] password = user_input[CONF_PASSWORD]
entry_id = self.context["entry_id"]
if entry := self.hass.config_entries.async_get_entry(entry_id): error = await self._async_validate_credentials(self._reauth_email, password)
error = await self._async_validate_credentials(email, password) if error is None:
if error is None: return self.async_update_reload_and_abort(
self.hass.config_entries.async_update_entry( self._get_reauth_entry(),
entry, data_updates=user_input,
data=entry.data | user_input, )
) errors["base"] = error
await self.hass.config_entries.async_reload(entry.entry_id)
return self.async_abort(reason="reauth_successful")
errors["base"] = error
return self.async_show_form( return self.async_show_form(
step_id="reauth_confirm", step_id="reauth_confirm",

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aosmith", "documentation": "https://www.home-assistant.io/integrations/aosmith",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["py-aosmith==1.0.8"] "requirements": ["py-aosmith==1.0.10"]
} }

View File

@ -3,7 +3,7 @@
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from py_aosmith.models import Device as AOSmithDevice, HotWaterStatus from py_aosmith.models import Device as AOSmithDevice
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -11,7 +11,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.const import UnitOfEnergy from homeassistant.const import PERCENTAGE, UnitOfEnergy
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -31,20 +31,11 @@ STATUS_ENTITY_DESCRIPTIONS: tuple[AOSmithStatusSensorEntityDescription, ...] = (
AOSmithStatusSensorEntityDescription( AOSmithStatusSensorEntityDescription(
key="hot_water_availability", key="hot_water_availability",
translation_key="hot_water_availability", translation_key="hot_water_availability",
device_class=SensorDeviceClass.ENUM, native_unit_of_measurement=PERCENTAGE,
options=["low", "medium", "high"], value_fn=lambda device: device.status.hot_water_status,
value_fn=lambda device: HOT_WATER_STATUS_MAP.get(
device.status.hot_water_status
),
), ),
) )
HOT_WATER_STATUS_MAP: dict[HotWaterStatus, str] = {
HotWaterStatus.LOW: "low",
HotWaterStatus.MEDIUM: "medium",
HotWaterStatus.HIGH: "high",
}
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -28,12 +28,7 @@
"entity": { "entity": {
"sensor": { "sensor": {
"hot_water_availability": { "hot_water_availability": {
"name": "Hot water availability", "name": "Hot water availability"
"state": {
"low": "Low",
"medium": "Medium",
"high": "High"
}
}, },
"energy_usage": { "energy_usage": {
"name": "Energy usage" "name": "Energy usage"

View File

@ -38,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema(
vol.Required(CONF_TOPIC): cv.string, vol.Required(CONF_TOPIC): cv.string,
vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA,
vol.Optional(CONF_SECURITY_PROTOCOL, default="PLAINTEXT"): vol.In( vol.Optional(CONF_SECURITY_PROTOCOL, default="PLAINTEXT"): vol.In(
["PLAINTEXT", "SASL_SSL"] ["PLAINTEXT", "SSL", "SASL_SSL"]
), ),
vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD): cv.string,
@ -53,7 +53,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Activate the Apache Kafka integration.""" """Activate the Apache Kafka integration."""
conf = config[DOMAIN] conf = config[DOMAIN]
kafka = hass.data[DOMAIN] = KafkaManager( kafka = KafkaManager(
hass, hass,
conf[CONF_IP_ADDRESS], conf[CONF_IP_ADDRESS],
conf[CONF_PORT], conf[CONF_PORT],
@ -94,7 +94,7 @@ class KafkaManager:
port: int, port: int,
topic: str, topic: str,
entities_filter: EntityFilter, entities_filter: EntityFilter,
security_protocol: Literal["PLAINTEXT", "SASL_SSL"], security_protocol: Literal["PLAINTEXT", "SSL", "SASL_SSL"],
username: str | None, username: str | None,
password: str | None, password: str | None,
) -> None: ) -> None:

Some files were not shown because too many files have changed in this diff Show More