Compare commits

..

1 commit

Author SHA1 Message Date
G Johansson
66ba63d61c Allow Filter title to be translated 2024-10-21 18:13:55 +00:00
1888 changed files with 20088 additions and 78599 deletions

View file

@ -79,7 +79,6 @@ components: &components
- homeassistant/components/group/** - homeassistant/components/group/**
- homeassistant/components/hassio/** - homeassistant/components/hassio/**
- homeassistant/components/homeassistant/** - homeassistant/components/homeassistant/**
- homeassistant/components/homeassistant_hardware/**
- homeassistant/components/http/** - homeassistant/components/http/**
- homeassistant/components/image/** - homeassistant/components/image/**
- homeassistant/components/input_boolean/** - homeassistant/components/input_boolean/**

View file

@ -58,13 +58,7 @@
], ],
"[python]": { "[python]": {
"editor.defaultFormatter": "charliermarsh.ruff" "editor.defaultFormatter": "charliermarsh.ruff"
}, }
"json.schemas": [
{
"fileMatch": ["homeassistant/components/*/manifest.json"],
"url": "./script/json_schemas/manifest_schema.json"
}
]
} }
} }
} }

3
.github/FUNDING.yml vendored
View file

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

View file

@ -10,7 +10,7 @@ on:
env: env:
BUILD_TYPE: core BUILD_TYPE: core
DEFAULT_PYTHON: "3.13" DEFAULT_PYTHON: "3.12"
PIP_TIMEOUT: 60 PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60 UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true" UV_SYSTEM_PYTHON: "true"
@ -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.2.2 uses: actions/checkout@v4.2.1
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.3.0 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -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.2.2 uses: actions/checkout@v4.2.1
- 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.3.0 uses: actions/setup-python@v5.2.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.2.2 uses: actions/checkout@v4.2.1
- 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.2.2 uses: actions/checkout@v4.2.1
- name: Initialize git - name: Initialize git
uses: home-assistant/actions/helpers/git-init@master uses: home-assistant/actions/helpers/git-init@master
@ -321,7 +321,7 @@ 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.2.2 uses: actions/checkout@v4.2.1
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@v3.7.0 uses: sigstore/cosign-installer@v3.7.0
@ -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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- 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
@ -531,7 +531,7 @@ jobs:
- name: Generate artifact attestation - name: Generate artifact attestation
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3
with: with:
subject-name: ${{ env.HASSFEST_IMAGE_NAME }} subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }} subject-digest: ${{ steps.push.outputs.digest }}

View file

@ -40,9 +40,9 @@ 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.12" HA_SHORT_VERSION: "2024.11"
DEFAULT_PYTHON: "3.12" DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12', '3.13']" ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022) # - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support # 10.6 is the current long-term-support
@ -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.2.2 uses: actions/checkout@v4.2.1
- 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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache@v4.1.1
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.1.2 uses: actions/cache@v4.1.1
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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache/restore@v4.1.1
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.1.2 uses: actions/cache/restore@v4.1.1
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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache/restore@v4.1.1
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.1.2 uses: actions/cache/restore@v4.1.1
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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache/restore@v4.1.1
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.1.2 uses: actions/cache/restore@v4.1.1
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.2.2 uses: actions/checkout@v4.2.1
- 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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache@v4.1.1
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.1.2 uses: actions/cache@v4.1.1
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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache/restore@v4.1.1
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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache/restore@v4.1.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -615,41 +615,37 @@ 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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.1.2 uses: actions/cache/restore@v4.1.1
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: Extract license data - name: Run pip-licenses
run: | run: |
. venv/bin/activate . venv/bin/activate
python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json pip-licenses --format=json --output-file=licenses.json
- name: Upload licenses - name: Upload licenses
uses: actions/upload-artifact@v4.4.3 uses: actions/upload-artifact@v4.4.3
with: with:
name: licenses-${{ github.run_number }}-${{ matrix.python-version }} name: licenses
path: licenses-${{ matrix.python-version }}.json path: licenses.json
- name: Check licenses - name: Process licenses
run: | run: |
. venv/bin/activate . venv/bin/activate
python -m script.licenses check licenses-${{ matrix.python-version }}.json python -m script.licenses licenses.json
pylint: pylint:
name: Check pylint name: Check pylint
@ -664,16 +660,16 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache/restore@v4.1.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -711,16 +707,16 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache/restore@v4.1.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -756,10 +752,10 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -772,7 +768,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.1.2 uses: actions/cache/restore@v4.1.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -780,7 +776,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.1.2 uses: actions/cache@v4.1.1
with: with:
path: .mypy_cache path: .mypy_cache
key: >- key: >-
@ -819,7 +815,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: |
@ -831,16 +831,16 @@ jobs:
libturbojpeg \ libturbojpeg \
libgammu-dev libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache/restore@v4.1.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -854,7 +854,7 @@ jobs:
- name: Upload pytest_buckets - name: Upload pytest_buckets
uses: actions/upload-artifact@v4.4.3 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
@ -895,16 +895,16 @@ jobs:
libturbojpeg \ libturbojpeg \
libgammu-dev libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache/restore@v4.1.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -919,7 +919,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
@ -945,7 +945,6 @@ jobs:
--timeout=9 \ --timeout=9 \
--durations=10 \ --durations=10 \
--numprocesses auto \ --numprocesses auto \
--snapshot-details \
--dist=loadfile \ --dist=loadfile \
${cov_params[@]} \ ${cov_params[@]} \
-o console_output_style=count \ -o console_output_style=count \
@ -1016,16 +1015,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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache/restore@v4.1.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -1068,7 +1067,6 @@ jobs:
-qq \ -qq \
--timeout=20 \ --timeout=20 \
--numprocesses 1 \ --numprocesses 1 \
--snapshot-details \
${cov_params[@]} \ ${cov_params[@]} \
-o console_output_style=count \ -o console_output_style=count \
--durations=10 \ --durations=10 \
@ -1100,7 +1098,7 @@ jobs:
./script/check_dirty ./script/check_dirty
pytest-postgres: pytest-postgres:
runs-on: ubuntu-24.04 runs-on: ubuntu-22.04
services: services:
postgres: postgres:
image: ${{ matrix.postgresql-group }} image: ${{ matrix.postgresql-group }}
@ -1140,21 +1138,19 @@ 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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache/restore@v4.1.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -1197,7 +1193,6 @@ jobs:
-qq \ -qq \
--timeout=9 \ --timeout=9 \
--numprocesses 1 \ --numprocesses 1 \
--snapshot-details \
${cov_params[@]} \ ${cov_params[@]} \
-o console_output_style=count \ -o console_output_style=count \
--durations=0 \ --durations=0 \
@ -1241,7 +1236,7 @@ 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.2.2 uses: actions/checkout@v4.2.1
- 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:
@ -1292,16 +1287,16 @@ jobs:
libturbojpeg \ libturbojpeg \
libgammu-dev libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.1.2 uses: actions/cache/restore@v4.1.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -1344,7 +1339,6 @@ jobs:
-qq \ -qq \
--timeout=9 \ --timeout=9 \
--numprocesses auto \ --numprocesses auto \
--snapshot-details \
${cov_params[@]} \ ${cov_params[@]} \
-o console_output_style=count \ -o console_output_style=count \
--durations=0 \ --durations=0 \
@ -1380,7 +1374,7 @@ 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.2.2 uses: actions/checkout@v4.2.1
- 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:

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.2.2 uses: actions/checkout@v4.2.1
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3.27.3 uses: github/codeql-action/init@v3.26.13
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.27.3 uses: github/codeql-action/analyze@v3.26.13
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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.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.2.2 uses: actions/checkout@v4.2.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.3.0 uses: actions/setup-python@v5.2.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -112,11 +112,11 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
abi: ["cp312", "cp313"] abi: ["cp312"]
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.2.2 uses: actions/checkout@v4.2.1
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.8 uses: actions/download-artifact@v4.1.8
@ -135,14 +135,14 @@ jobs:
sed -i "/uv/d" requirements_diff.txt sed -i "/uv/d" requirements_diff.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2024.11.0 uses: home-assistant/wheels@2024.07.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev" apk: "libffi-dev;openssl-dev;yaml-dev;nasm"
skip-binary: aiohttp;multidict;yarl skip-binary: aiohttp;multidict;yarl
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
@ -156,11 +156,11 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
abi: ["cp312", "cp313"] abi: ["cp312"]
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.2.2 uses: actions/checkout@v4.2.1
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.8 uses: actions/download-artifact@v4.1.8
@ -198,7 +198,6 @@ jobs:
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt
- name: Create requirements for cython<3 - name: Create requirements for cython<3
if: matrix.abi == 'cp312'
run: | run: |
# 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.
@ -209,8 +208,7 @@ jobs:
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)
uses: home-assistant/wheels@2024.11.0 uses: home-assistant/wheels@2024.07.1
if: matrix.abi == 'cp312'
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
@ -225,43 +223,43 @@ jobs:
pip: "'cython<3'" pip: "'cython<3'"
- name: Build wheels (part 1) - name: Build wheels (part 1)
uses: home-assistant/wheels@2024.11.0 uses: home-assistant/wheels@2024.07.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
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;zlib-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;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;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"
- name: Build wheels (part 2) - name: Build wheels (part 2)
uses: home-assistant/wheels@2024.11.0 uses: home-assistant/wheels@2024.07.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
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;zlib-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;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;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"
- name: Build wheels (part 3) - name: Build wheels (part 3)
uses: home-assistant/wheels@2024.11.0 uses: home-assistant/wheels@2024.07.1
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
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;zlib-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;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;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"

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.7.3 rev: v0.7.0
hooks: hooks:
- id: ruff - id: ruff
args: args:
@ -90,7 +90,7 @@ repos:
pass_filenames: false pass_filenames: false
language: script language: script
types: [text] types: [text]
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$ files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml)$
- id: hassfest-mypy-config - id: hassfest-mypy-config
name: hassfest-mypy-config name: hassfest-mypy-config
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config

View file

@ -124,7 +124,6 @@ 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.*
@ -209,7 +208,6 @@ 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.*
@ -324,13 +322,11 @@ homeassistant.components.moon.*
homeassistant.components.mopeka.* homeassistant.components.mopeka.*
homeassistant.components.motionmount.* homeassistant.components.motionmount.*
homeassistant.components.mqtt.* homeassistant.components.mqtt.*
homeassistant.components.music_assistant.*
homeassistant.components.my.* homeassistant.components.my.*
homeassistant.components.mysensors.* homeassistant.components.mysensors.*
homeassistant.components.myuplink.* homeassistant.components.myuplink.*
homeassistant.components.nam.* homeassistant.components.nam.*
homeassistant.components.nanoleaf.* homeassistant.components.nanoleaf.*
homeassistant.components.nasweb.*
homeassistant.components.neato.* homeassistant.components.neato.*
homeassistant.components.nest.* homeassistant.components.nest.*
homeassistant.components.netatmo.* homeassistant.components.netatmo.*
@ -340,7 +336,6 @@ homeassistant.components.nfandroidtv.*
homeassistant.components.nightscout.* homeassistant.components.nightscout.*
homeassistant.components.nissan_leaf.* homeassistant.components.nissan_leaf.*
homeassistant.components.no_ip.* homeassistant.components.no_ip.*
homeassistant.components.nordpool.*
homeassistant.components.notify.* homeassistant.components.notify.*
homeassistant.components.notion.* homeassistant.components.notion.*
homeassistant.components.number.* homeassistant.components.number.*

View file

@ -6,13 +6,5 @@
// 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

@ -40,8 +40,6 @@ build.json @home-assistant/supervisor
# Integrations # Integrations
/homeassistant/components/abode/ @shred86 /homeassistant/components/abode/ @shred86
/tests/components/abode/ @shred86 /tests/components/abode/ @shred86
/homeassistant/components/acaia/ @zweckj
/tests/components/acaia/ @zweckj
/homeassistant/components/accuweather/ @bieniu /homeassistant/components/accuweather/ @bieniu
/tests/components/accuweather/ @bieniu /tests/components/accuweather/ @bieniu
/homeassistant/components/acmeda/ @atmurray /homeassistant/components/acmeda/ @atmurray
@ -498,8 +496,8 @@ build.json @home-assistant/supervisor
/tests/components/freebox/ @hacf-fr @Quentame /tests/components/freebox/ @hacf-fr @Quentame
/homeassistant/components/freedompro/ @stefano055415 /homeassistant/components/freedompro/ @stefano055415
/tests/components/freedompro/ @stefano055415 /tests/components/freedompro/ @stefano055415
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185 /homeassistant/components/fritz/ @mammuth @AaronDavidSchneider @chemelli74 @mib1185
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185 /tests/components/fritz/ @mammuth @AaronDavidSchneider @chemelli74 @mib1185
/homeassistant/components/fritzbox/ @mib1185 @flabbamann /homeassistant/components/fritzbox/ @mib1185 @flabbamann
/tests/components/fritzbox/ @mib1185 @flabbamann /tests/components/fritzbox/ @mib1185 @flabbamann
/homeassistant/components/fritzbox_callmonitor/ @cdce8p /homeassistant/components/fritzbox_callmonitor/ @cdce8p
@ -619,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 @Diegorro98 /homeassistant/components/home_connect/ @DavidMStraub
/tests/components/home_connect/ @DavidMStraub @Diegorro98 /tests/components/home_connect/ @DavidMStraub
/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
@ -661,8 +659,6 @@ 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
@ -823,8 +819,6 @@ 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
@ -956,8 +950,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/msteams/ @peroyvind /homeassistant/components/msteams/ @peroyvind
/homeassistant/components/mullvad/ @meichthys /homeassistant/components/mullvad/ @meichthys
/tests/components/mullvad/ @meichthys /tests/components/mullvad/ @meichthys
/homeassistant/components/music_assistant/ @music-assistant
/tests/components/music_assistant/ @music-assistant
/homeassistant/components/mutesync/ @currentoor /homeassistant/components/mutesync/ @currentoor
/tests/components/mutesync/ @currentoor /tests/components/mutesync/ @currentoor
/homeassistant/components/my/ @home-assistant/core /homeassistant/components/my/ @home-assistant/core
@ -972,8 +964,6 @@ build.json @home-assistant/supervisor
/tests/components/nam/ @bieniu /tests/components/nam/ @bieniu
/homeassistant/components/nanoleaf/ @milanmeu @joostlek /homeassistant/components/nanoleaf/ @milanmeu @joostlek
/tests/components/nanoleaf/ @milanmeu @joostlek /tests/components/nanoleaf/ @milanmeu @joostlek
/homeassistant/components/nasweb/ @nasWebio
/tests/components/nasweb/ @nasWebio
/homeassistant/components/neato/ @Santobert /homeassistant/components/neato/ @Santobert
/tests/components/neato/ @Santobert /tests/components/neato/ @Santobert
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM /homeassistant/components/nederlandse_spoorwegen/ @YarmoM
@ -1014,8 +1004,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/noaa_tides/ @jdelaney72 /homeassistant/components/noaa_tides/ @jdelaney72
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe /homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
/tests/components/nobo_hub/ @echoromeo @oyvindwe /tests/components/nobo_hub/ @echoromeo @oyvindwe
/homeassistant/components/nordpool/ @gjohansson-ST
/tests/components/nordpool/ @gjohansson-ST
/homeassistant/components/notify/ @home-assistant/core /homeassistant/components/notify/ @home-assistant/core
/tests/components/notify/ @home-assistant/core /tests/components/notify/ @home-assistant/core
/homeassistant/components/notify_events/ @matrozov @papajojo /homeassistant/components/notify_events/ @matrozov @papajojo
@ -1059,7 +1047,6 @@ 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
@ -1101,8 +1088,6 @@ 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/peco/ @IceBotYT /homeassistant/components/peco/ @IceBotYT
@ -1346,8 +1331,6 @@ build.json @home-assistant/supervisor
/tests/components/siren/ @home-assistant/core @raman325 /tests/components/siren/ @home-assistant/core @raman325
/homeassistant/components/sisyphus/ @jkeljo /homeassistant/components/sisyphus/ @jkeljo
/homeassistant/components/sky_hub/ @rogerselwyn /homeassistant/components/sky_hub/ @rogerselwyn
/homeassistant/components/sky_remote/ @dunnmj @saty9
/tests/components/sky_remote/ @dunnmj @saty9
/homeassistant/components/skybell/ @tkdrob /homeassistant/components/skybell/ @tkdrob
/tests/components/skybell/ @tkdrob /tests/components/skybell/ @tkdrob
/homeassistant/components/slack/ @tkdrob @fletcherau /homeassistant/components/slack/ @tkdrob @fletcherau
@ -1366,7 +1349,6 @@ 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
@ -1430,8 +1412,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 @jb101010-2 /homeassistant/components/suez_water/ @ooii
/tests/components/suez_water/ @ooii @jb101010-2 /tests/components/suez_water/ @ooii
/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
@ -1489,8 +1471,8 @@ build.json @home-assistant/supervisor
/tests/components/tedee/ @patrickhilker @zweckj /tests/components/tedee/ @patrickhilker @zweckj
/homeassistant/components/tellduslive/ @fredrike /homeassistant/components/tellduslive/ @fredrike
/tests/components/tellduslive/ @fredrike /tests/components/tellduslive/ @fredrike
/homeassistant/components/template/ @PhracturedBlue @home-assistant/core /homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core
/tests/components/template/ @PhracturedBlue @home-assistant/core /tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
/homeassistant/components/tesla_fleet/ @Bre77 /homeassistant/components/tesla_fleet/ @Bre77
/tests/components/tesla_fleet/ @Bre77 /tests/components/tesla_fleet/ @Bre77
/homeassistant/components/tesla_wall_connector/ @einarhauks /homeassistant/components/tesla_wall_connector/ @einarhauks

View file

@ -7,13 +7,12 @@ 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.5.0 RUN pip3 install uv==0.4.22
WORKDIR /usr/src WORKDIR /usr/src
@ -55,7 +54,7 @@ RUN \
"armv7") go2rtc_suffix='arm' ;; \ "armv7") go2rtc_suffix='arm' ;; \
*) go2rtc_suffix=${BUILD_ARCH} ;; \ *) go2rtc_suffix=${BUILD_ARCH} ;; \
esac \ esac \
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.7/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.4/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& chmod +x /bin/go2rtc \ && chmod +x /bin/go2rtc \
# Verify go2rtc can be executed # Verify go2rtc can be executed
&& go2rtc --version && go2rtc --version

View file

@ -35,9 +35,6 @@ RUN \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Add go2rtc binary
COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc
# Install uv # Install uv
RUN pip3 install uv RUN pip3 install uv

View file

@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant image: ghcr.io/home-assistant/{arch}-homeassistant
build_from: build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.11.0 aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.06.1
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.11.0 armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.06.1
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.11.0 armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.06.1
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.11.0 amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.06.1
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.11.0 i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.06.1
codenotary: codenotary:
signer: notary@home-assistant.io signer: notary@home-assistant.io
base_image: notary@home-assistant.io base_image: notary@home-assistant.io

View file

@ -9,7 +9,6 @@ import os
import sys import sys
import threading import threading
from .backup_restore import restore_backup
from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
FAULT_LOG_FILENAME = "home-assistant.log.fault" FAULT_LOG_FILENAME = "home-assistant.log.fault"
@ -183,9 +182,6 @@ def main() -> int:
return scripts.run(args.script) return scripts.run(args.script)
config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config)) config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config))
if restore_backup(config_dir):
return RESTART_EXIT_CODE
ensure_config_path(config_dir) ensure_config_path(config_dir)
# pylint: disable-next=import-outside-toplevel # pylint: disable-next=import-outside-toplevel

View file

@ -1,126 +0,0 @@
"""Home Assistant module to handle restoring backups."""
from dataclasses import dataclass
import json
import logging
from pathlib import Path
import shutil
import sys
from tempfile import TemporaryDirectory
from awesomeversion import AwesomeVersion
import securetar
from .const import __version__ as HA_VERSION
RESTORE_BACKUP_FILE = ".HA_RESTORE"
KEEP_PATHS = ("backups",)
_LOGGER = logging.getLogger(__name__)
@dataclass
class RestoreBackupFileContent:
"""Definition for restore backup file content."""
backup_file_path: Path
def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent | None:
"""Return the contents of the restore backup file."""
instruction_path = config_dir.joinpath(RESTORE_BACKUP_FILE)
try:
instruction_content = json.loads(instruction_path.read_text(encoding="utf-8"))
return RestoreBackupFileContent(
backup_file_path=Path(instruction_content["path"])
)
except (FileNotFoundError, json.JSONDecodeError):
return None
def _clear_configuration_directory(config_dir: Path) -> None:
"""Delete all files and directories in the config directory except for the backups directory."""
keep_paths = [config_dir.joinpath(path) for path in KEEP_PATHS]
config_contents = sorted(
[entry for entry in config_dir.iterdir() if entry not in keep_paths]
)
for entry in config_contents:
entrypath = config_dir.joinpath(entry)
if entrypath.is_file():
entrypath.unlink()
elif entrypath.is_dir():
shutil.rmtree(entrypath)
def _extract_backup(config_dir: Path, backup_file_path: Path) -> None:
"""Extract the backup file to the config directory."""
with (
TemporaryDirectory() as tempdir,
securetar.SecureTarFile(
backup_file_path,
gzip=False,
mode="r",
) as ostf,
):
ostf.extractall(
path=Path(tempdir, "extracted"),
members=securetar.secure_path(ostf),
filter="fully_trusted",
)
backup_meta_file = Path(tempdir, "extracted", "backup.json")
backup_meta = json.loads(backup_meta_file.read_text(encoding="utf8"))
if (
backup_meta_version := AwesomeVersion(
backup_meta["homeassistant"]["version"]
)
) > HA_VERSION:
raise ValueError(
f"You need at least Home Assistant version {backup_meta_version} to restore this backup"
)
with securetar.SecureTarFile(
Path(
tempdir,
"extracted",
f"homeassistant.tar{'.gz' if backup_meta["compressed"] else ''}",
),
gzip=backup_meta["compressed"],
mode="r",
) as istf:
for member in istf.getmembers():
if member.name == "data":
continue
member.name = member.name.replace("data/", "")
_clear_configuration_directory(config_dir)
istf.extractall(
path=config_dir,
members=[
member
for member in securetar.secure_path(istf)
if member.name != "data"
],
filter="fully_trusted",
)
def restore_backup(config_dir_path: str) -> bool:
"""Restore the backup file if any.
Returns True if a restore backup file was found and restored, False otherwise.
"""
config_dir = Path(config_dir_path)
if not (restore_content := restore_backup_file_content(config_dir)):
return False
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
backup_file_path = restore_content.backup_file_path
_LOGGER.info("Restoring %s", backup_file_path)
try:
_extract_backup(config_dir, backup_file_path)
except FileNotFoundError as err:
raise ValueError(f"Backup file {backup_file_path} does not exist") from err
_LOGGER.info("Restore complete, restarting")
return True

View file

@ -70,7 +70,6 @@ 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,
@ -480,7 +479,7 @@ async def async_from_config_dict(
core_config = config.get(core.DOMAIN, {}) core_config = config.get(core.DOMAIN, {})
try: try:
await async_process_ha_core_config(hass, core_config) await conf_util.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)
@ -515,7 +514,7 @@ async def async_from_config_dict(
issue_registry.async_create_issue( issue_registry.async_create_issue(
hass, hass,
core.DOMAIN, core.DOMAIN,
f"python_version_{required_python_version}", "python_version",
is_fixable=False, is_fixable=False,
severity=issue_registry.IssueSeverity.WARNING, severity=issue_registry.IssueSeverity.WARNING,
breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE, breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE,

View file

@ -1,5 +0,0 @@
{
"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", "lg_thinq", "webostv"] "integrations": ["lg_netcast", "lg_soundbar", "webostv"]
} }

View file

@ -1,5 +0,0 @@
{
"domain": "sky",
"name": "Sky",
"integrations": ["sky_hub", "sky_remote"]
}

View file

@ -7,9 +7,13 @@ 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
@ -40,14 +44,14 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanelEntity):
_device: Alarm _device: Alarm
@property @property
def alarm_state(self) -> AlarmControlPanelState | None: def state(self) -> str | None:
"""Return the state of the device.""" """Return the state of the device."""
if self._device.is_standby: if self._device.is_standby:
return AlarmControlPanelState.DISARMED return STATE_ALARM_DISARMED
if self._device.is_away: if self._device.is_away:
return AlarmControlPanelState.ARMED_AWAY return STATE_ALARM_ARMED_AWAY
if self._device.is_home: if self._device.is_home:
return AlarmControlPanelState.ARMED_HOME return STATE_ALARM_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

@ -1,29 +0,0 @@
"""Initialize the Acaia component."""
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import AcaiaConfigEntry, AcaiaCoordinator
PLATFORMS = [
Platform.BUTTON,
]
async def async_setup_entry(hass: HomeAssistant, entry: AcaiaConfigEntry) -> bool:
"""Set up acaia as config entry."""
coordinator = AcaiaCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AcaiaConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View file

@ -1,61 +0,0 @@
"""Button entities for Acaia scales."""
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from aioacaia.acaiascale import AcaiaScale
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .coordinator import AcaiaConfigEntry
from .entity import AcaiaEntity
@dataclass(kw_only=True, frozen=True)
class AcaiaButtonEntityDescription(ButtonEntityDescription):
"""Description for acaia button entities."""
press_fn: Callable[[AcaiaScale], Coroutine[Any, Any, None]]
BUTTONS: tuple[AcaiaButtonEntityDescription, ...] = (
AcaiaButtonEntityDescription(
key="tare",
translation_key="tare",
press_fn=lambda scale: scale.tare(),
),
AcaiaButtonEntityDescription(
key="reset_timer",
translation_key="reset_timer",
press_fn=lambda scale: scale.reset_timer(),
),
AcaiaButtonEntityDescription(
key="start_stop",
translation_key="start_stop",
press_fn=lambda scale: scale.start_stop_timer(),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AcaiaConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up button entities and services."""
coordinator = entry.runtime_data
async_add_entities(AcaiaButton(coordinator, description) for description in BUTTONS)
class AcaiaButton(AcaiaEntity, ButtonEntity):
"""Representation of an Acaia button."""
entity_description: AcaiaButtonEntityDescription
async def async_press(self) -> None:
"""Handle the button press."""
await self.entity_description.press_fn(self._scale)

View file

@ -1,149 +0,0 @@
"""Config flow for Acaia integration."""
import logging
from typing import Any
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError, AcaiaUnknownDevice
from aioacaia.helpers import is_new_scale
import voluptuous as vol
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ADDRESS, CONF_NAME
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import CONF_IS_NEW_STYLE_SCALE, DOMAIN
_LOGGER = logging.getLogger(__name__)
class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for acaia."""
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovered: dict[str, Any] = {}
self._discovered_devices: dict[str, str] = {}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input is not None:
mac = format_mac(user_input[CONF_ADDRESS])
try:
is_new_style_scale = await is_new_scale(mac)
except AcaiaDeviceNotFound:
errors["base"] = "device_not_found"
except AcaiaError:
_LOGGER.exception("Error occurred while connecting to the scale")
errors["base"] = "unknown"
except AcaiaUnknownDevice:
return self.async_abort(reason="unsupported_device")
else:
await self.async_set_unique_id(mac)
self._abort_if_unique_id_configured()
if not errors:
return self.async_create_entry(
title=self._discovered_devices[user_input[CONF_ADDRESS]],
data={
CONF_ADDRESS: mac,
CONF_IS_NEW_STYLE_SCALE: is_new_style_scale,
},
)
for device in async_discovered_service_info(self.hass):
self._discovered_devices[device.address] = device.name
if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")
options = [
SelectOptionDict(
value=device_mac,
label=f"{device_name} ({device_mac})",
)
for device_mac, device_name in self._discovered_devices.items()
]
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_ADDRESS): SelectSelector(
SelectSelectorConfig(
options=options,
mode=SelectSelectorMode.DROPDOWN,
)
)
}
),
errors=errors,
)
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
) -> ConfigFlowResult:
"""Handle a discovered Bluetooth device."""
self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address)
self._discovered[CONF_NAME] = discovery_info.name
await self.async_set_unique_id(mac)
self._abort_if_unique_id_configured()
try:
self._discovered[CONF_IS_NEW_STYLE_SCALE] = await is_new_scale(
discovery_info.address
)
except AcaiaDeviceNotFound:
_LOGGER.debug("Device not found during discovery")
return self.async_abort(reason="device_not_found")
except AcaiaError:
_LOGGER.debug(
"Error occurred while connecting to the scale during discovery",
exc_info=True,
)
return self.async_abort(reason="unknown")
except AcaiaUnknownDevice:
_LOGGER.debug("Unsupported device during discovery")
return self.async_abort(reason="unsupported_device")
return await self.async_step_bluetooth_confirm()
async def async_step_bluetooth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle confirmation of Bluetooth discovery."""
if user_input is not None:
return self.async_create_entry(
title=self._discovered[CONF_NAME],
data={
CONF_ADDRESS: self._discovered[CONF_ADDRESS],
CONF_IS_NEW_STYLE_SCALE: self._discovered[CONF_IS_NEW_STYLE_SCALE],
},
)
self.context["title_placeholders"] = placeholders = {
CONF_NAME: self._discovered[CONF_NAME]
}
self._set_confirm_only()
return self.async_show_form(
step_id="bluetooth_confirm",
description_placeholders=placeholders,
)

View file

@ -1,4 +0,0 @@
"""Constants for component."""
DOMAIN = "acaia"
CONF_IS_NEW_STYLE_SCALE = "is_new_style_scale"

View file

@ -1,86 +0,0 @@
"""Coordinator for Acaia integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from aioacaia.acaiascale import AcaiaScale
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import CONF_IS_NEW_STYLE_SCALE
SCAN_INTERVAL = timedelta(seconds=15)
_LOGGER = logging.getLogger(__name__)
type AcaiaConfigEntry = ConfigEntry[AcaiaCoordinator]
class AcaiaCoordinator(DataUpdateCoordinator[None]):
"""Class to handle fetching data from the scale."""
config_entry: AcaiaConfigEntry
def __init__(self, hass: HomeAssistant, entry: AcaiaConfigEntry) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
_LOGGER,
name="acaia coordinator",
update_interval=SCAN_INTERVAL,
config_entry=entry,
)
self._scale = AcaiaScale(
address_or_ble_device=entry.data[CONF_ADDRESS],
name=entry.title,
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
notify_callback=self.async_update_listeners,
)
@property
def scale(self) -> AcaiaScale:
"""Return the scale object."""
return self._scale
async def _async_update_data(self) -> None:
"""Fetch data."""
# scale is already connected, return
if self._scale.connected:
return
# scale is not connected, try to connect
try:
await self._scale.connect(setup_tasks=False)
except (AcaiaDeviceNotFound, AcaiaError, TimeoutError) as ex:
_LOGGER.debug(
"Could not connect to scale: %s, Error: %s",
self.config_entry.data[CONF_ADDRESS],
ex,
)
self._scale.device_disconnected_handler(notify=False)
return
# connected, set up background tasks
if not self._scale.heartbeat_task or self._scale.heartbeat_task.done():
self._scale.heartbeat_task = self.config_entry.async_create_background_task(
hass=self.hass,
target=self._scale.send_heartbeats(),
name="acaia_heartbeat_task",
)
if not self._scale.process_queue_task or self._scale.process_queue_task.done():
self._scale.process_queue_task = (
self.config_entry.async_create_background_task(
hass=self.hass,
target=self._scale.process_queue(),
name="acaia_process_queue_task",
)
)

View file

@ -1,40 +0,0 @@
"""Base class for Acaia entities."""
from dataclasses import dataclass
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AcaiaCoordinator
@dataclass
class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]):
"""Common elements for all entities."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AcaiaCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self.entity_description = entity_description
self._scale = coordinator.scale
self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._scale.mac)},
manufacturer="Acaia",
model=self._scale.model,
suggested_area="Kitchen",
)
@property
def available(self) -> bool:
"""Returns whether entity is available."""
return super().available and self._scale.connected

View file

@ -1,15 +0,0 @@
{
"entity": {
"button": {
"tare": {
"default": "mdi:scale-balance"
},
"reset_timer": {
"default": "mdi:timer-refresh"
},
"start_stop": {
"default": "mdi:timer-play"
}
}
}
}

View file

@ -1,29 +0,0 @@
{
"domain": "acaia",
"name": "Acaia",
"bluetooth": [
{
"manufacturer_id": 16962
},
{
"local_name": "ACAIA*"
},
{
"local_name": "PYXIS-*"
},
{
"local_name": "LUNAR-*"
},
{
"local_name": "PROCHBT001"
}
],
"codeowners": ["@zweckj"],
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/acaia",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["aioacaia"],
"requirements": ["aioacaia==0.1.6"]
}

View file

@ -1,38 +0,0 @@
{
"config": {
"flow_title": "{name}",
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"unsupported_device": "This device is not supported."
},
"error": {
"device_not_found": "Device could not be found.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
},
"user": {
"description": "[%key:component::bluetooth::config::step::user::description%]",
"data": {
"address": "[%key:common::config_flow::data::device%]"
}
}
}
},
"entity": {
"button": {
"tare": {
"name": "Tare"
},
"reset_timer": {
"name": "Reset timer"
},
"start_stop": {
"name": "Start/stop timer"
}
}
}
}

View file

@ -7,6 +7,7 @@ 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,
@ -17,7 +18,6 @@ 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,7 +55,6 @@ 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

@ -1,5 +1,6 @@
"""The AEMET OpenData component.""" """The AEMET OpenData component."""
from dataclasses import dataclass
import logging import logging
from aemet_opendata.exceptions import AemetError, TownNotFound from aemet_opendata.exceptions import AemetError, TownNotFound
@ -12,10 +13,20 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .const import CONF_STATION_UPDATES, PLATFORMS from .const import CONF_STATION_UPDATES, PLATFORMS
from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator from .coordinator import WeatherUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type AemetConfigEntry = ConfigEntry[AemetData]
@dataclass
class AemetData:
"""Aemet runtime data."""
name: str
coordinator: WeatherUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> bool:
"""Set up AEMET OpenData as config entry.""" """Set up AEMET OpenData as config entry."""
@ -35,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
except AemetError as err: except AemetError as err:
raise ConfigEntryNotReady(err) from err raise ConfigEntryNotReady(err) from err
weather_coordinator = WeatherUpdateCoordinator(hass, entry, aemet) weather_coordinator = WeatherUpdateCoordinator(hass, aemet)
await weather_coordinator.async_config_entry_first_refresh() await weather_coordinator.async_config_entry_first_refresh()
entry.runtime_data = AemetData(name=name, coordinator=weather_coordinator) entry.runtime_data = AemetData(name=name, coordinator=weather_coordinator)

View file

@ -3,7 +3,6 @@
from __future__ import annotations 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 Any, Final, cast from typing import Any, Final, cast
@ -20,7 +19,6 @@ from aemet_opendata.helpers import dict_nested_value
from aemet_opendata.interface import AEMET from aemet_opendata.interface import AEMET
from homeassistant.components.weather import Forecast from homeassistant.components.weather import Forecast
from homeassistant.config_entries import ConfigEntry
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
@ -31,16 +29,6 @@ _LOGGER = logging.getLogger(__name__)
API_TIMEOUT: Final[int] = 120 API_TIMEOUT: Final[int] = 120
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10) WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
type AemetConfigEntry = ConfigEntry[AemetData]
@dataclass
class AemetData:
"""Aemet runtime data."""
name: str
coordinator: WeatherUpdateCoordinator
class WeatherUpdateCoordinator(DataUpdateCoordinator): class WeatherUpdateCoordinator(DataUpdateCoordinator):
"""Weather data update coordinator.""" """Weather data update coordinator."""
@ -48,7 +36,6 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
entry: AemetConfigEntry,
aemet: AEMET, aemet: AEMET,
) -> None: ) -> None:
"""Initialize coordinator.""" """Initialize coordinator."""
@ -57,7 +44,6 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
super().__init__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
config_entry=entry,
name=DOMAIN, name=DOMAIN,
update_interval=WEATHER_UPDATE_INTERVAL, update_interval=WEATHER_UPDATE_INTERVAL,
) )

View file

@ -15,7 +15,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .coordinator import AemetConfigEntry from . import AemetConfigEntry
TO_REDACT_CONFIG = [ TO_REDACT_CONFIG = [
CONF_API_KEY, CONF_API_KEY,

View file

@ -55,6 +55,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import AemetConfigEntry
from .const import ( from .const import (
ATTR_API_CONDITION, ATTR_API_CONDITION,
ATTR_API_FORECAST_CONDITION, ATTR_API_FORECAST_CONDITION,
@ -86,7 +87,7 @@ from .const import (
ATTR_API_WIND_SPEED, ATTR_API_WIND_SPEED,
CONDITIONS_MAP, CONDITIONS_MAP,
) )
from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator from .coordinator import WeatherUpdateCoordinator
from .entity import AemetEntity from .entity import AemetEntity
@ -248,7 +249,6 @@ 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,7 +263,6 @@ 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

@ -27,8 +27,9 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AemetConfigEntry
from .const import CONDITIONS_MAP from .const import CONDITIONS_MAP
from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator from .coordinator import WeatherUpdateCoordinator
from .entity import AemetEntity from .entity import AemetEntity

View file

@ -5,7 +5,12 @@ 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
@ -60,37 +65,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_alarm_state = None self._attr_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_alarm_state = AlarmControlPanelState.ARMED_AWAY self._attr_state = STATE_ALARM_ARMED_AWAY
if prof == CONF_HOME_MODE_NAME: if prof == CONF_HOME_MODE_NAME:
self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME self._attr_state = STATE_ALARM_ARMED_HOME
elif prof == CONF_NIGHT_MODE_NAME: elif prof == CONF_NIGHT_MODE_NAME:
self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT self._attr_state = STATE_ALARM_ARMED_NIGHT
else: else:
self._attr_alarm_state = AlarmControlPanelState.DISARMED self._attr_state = STATE_ALARM_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_alarm_state = AlarmControlPanelState.DISARMED self._attr_state = STATE_ALARM_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_alarm_state = AlarmControlPanelState.ARMED_AWAY self._attr_state = STATE_ALARM_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_alarm_state = AlarmControlPanelState.ARMED_HOME self._attr_state = STATE_ALARM_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_alarm_state = AlarmControlPanelState.ARMED_NIGHT self._attr_state = STATE_ALARM_ARMED_NIGHT

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/agent_dvr", "documentation": "https://www.home-assistant.io/integrations/agent_dvr",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["agent"], "loggers": ["agent"],
"requirements": ["agent-py==0.0.24"] "requirements": ["agent-py==0.0.23"]
} }

View file

@ -1,7 +1,5 @@
"""Config flow for AirNow integration.""" """Config flow for AirNow integration."""
from __future__ import annotations
import logging import logging
from typing import Any from typing import Any
@ -14,6 +12,7 @@ from homeassistant.config_entries import (
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlow,
OptionsFlowWithConfigEntry,
) )
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -121,12 +120,12 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
@callback @callback
def async_get_options_flow( def async_get_options_flow(
config_entry: ConfigEntry, config_entry: ConfigEntry,
) -> AirNowOptionsFlowHandler: ) -> OptionsFlow:
"""Return the options flow.""" """Return the options flow."""
return AirNowOptionsFlowHandler() return AirNowOptionsFlowHandler(config_entry)
class AirNowOptionsFlowHandler(OptionsFlow): class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle an options flow for AirNow.""" """Handle an options flow for AirNow."""
async def async_step_init( async def async_step_init(
@ -137,7 +136,12 @@ class AirNowOptionsFlowHandler(OptionsFlow):
return self.async_create_entry(data=user_input) return self.async_create_entry(data=user_input)
options_schema = vol.Schema( options_schema = vol.Schema(
{vol.Optional(CONF_RADIUS): vol.All(int, vol.Range(min=5))} {
vol.Optional(CONF_RADIUS): vol.All(
int,
vol.Range(min=5),
),
}
) )
return self.async_show_form( return self.async_show_form(

View file

@ -42,7 +42,6 @@ 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,27 +2,75 @@
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 MAX_RETRIES_AFTER_STARTUP from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, 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."""
coordinator = AirthingsBLEDataUpdateCoordinator(hass, entry) hass.data.setdefault(DOMAIN, {})
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.
coordinator.airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP) airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP)
entry.runtime_data = coordinator entry.runtime_data = coordinator

View file

@ -1,68 +0,0 @@
"""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.2"] "requirements": ["airthings-ble==0.9.1"]
} }

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,6 +9,8 @@ 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]
@ -17,6 +19,8 @@ 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,7 +204,6 @@ 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

@ -81,7 +81,6 @@ 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

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==0.9.6"] "requirements": ["aioairzone==0.9.5"]
} }

View file

@ -310,10 +310,6 @@ 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] = {
@ -337,6 +333,9 @@ 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."""
@ -367,10 +366,6 @@ 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] = {
@ -381,6 +376,9 @@ 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.10"] "requirements": ["aioairzone-cloud==0.6.7"]
} }

View file

@ -2,11 +2,10 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from datetime import timedelta from datetime import timedelta
from functools import partial from functools import partial
import logging import logging
from typing import TYPE_CHECKING, Any, Final, final from typing import Any, Final, final
from propcache import cached_property from propcache import cached_property
import voluptuous as vol import voluptuous as vol
@ -34,7 +33,6 @@ 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
@ -51,7 +49,6 @@ from .const import ( # noqa: F401
ATTR_CODE_ARM_REQUIRED, ATTR_CODE_ARM_REQUIRED,
DOMAIN, DOMAIN,
AlarmControlPanelEntityFeature, AlarmControlPanelEntityFeature,
AlarmControlPanelState,
CodeFormat, CodeFormat,
) )
@ -145,7 +142,6 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
"changed_by", "changed_by",
"code_arm_required", "code_arm_required",
"supported_features", "supported_features",
"alarm_state",
} }
@ -153,7 +149,6 @@ 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
@ -162,84 +157,6 @@ 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 not None:
return alarm_state
if self._attr_state is not None:
# Backwards compatibility for integrations that set state directly
# Should be removed in 2025.11
if TYPE_CHECKING:
assert isinstance(self._attr_state, str)
return self._attr_state
return None
@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,21 +17,6 @@ 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,6 +13,13 @@ 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 (
@ -24,7 +31,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, AlarmControlPanelState from . import DOMAIN
from .const import ( from .const import (
CONDITION_ARMED_AWAY, CONDITION_ARMED_AWAY,
CONDITION_ARMED_CUSTOM_BYPASS, CONDITION_ARMED_CUSTOM_BYPASS,
@ -102,19 +109,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 = AlarmControlPanelState.TRIGGERED state = STATE_ALARM_TRIGGERED
elif config[CONF_TYPE] == CONDITION_DISARMED: elif config[CONF_TYPE] == CONDITION_DISARMED:
state = AlarmControlPanelState.DISARMED state = STATE_ALARM_DISARMED
elif config[CONF_TYPE] == CONDITION_ARMED_HOME: elif config[CONF_TYPE] == CONDITION_ARMED_HOME:
state = AlarmControlPanelState.ARMED_HOME state = STATE_ALARM_ARMED_HOME
elif config[CONF_TYPE] == CONDITION_ARMED_AWAY: elif config[CONF_TYPE] == CONDITION_ARMED_AWAY:
state = AlarmControlPanelState.ARMED_AWAY state = STATE_ALARM_ARMED_AWAY
elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT: elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT:
state = AlarmControlPanelState.ARMED_NIGHT state = STATE_ALARM_ARMED_NIGHT
elif config[CONF_TYPE] == CONDITION_ARMED_VACATION: elif config[CONF_TYPE] == CONDITION_ARMED_VACATION:
state = AlarmControlPanelState.ARMED_VACATION state = STATE_ALARM_ARMED_VACATION
elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS: elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS:
state = AlarmControlPanelState.ARMED_CUSTOM_BYPASS state = STATE_ALARM_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,6 +15,13 @@ 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
@ -22,7 +29,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, AlarmControlPanelState from . import DOMAIN
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"}
@ -122,19 +129,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 = AlarmControlPanelState.TRIGGERED to_state = STATE_ALARM_TRIGGERED
elif config[CONF_TYPE] == "disarmed": elif config[CONF_TYPE] == "disarmed":
to_state = AlarmControlPanelState.DISARMED to_state = STATE_ALARM_DISARMED
elif config[CONF_TYPE] == "arming": elif config[CONF_TYPE] == "arming":
to_state = AlarmControlPanelState.ARMING to_state = STATE_ALARM_ARMING
elif config[CONF_TYPE] == "armed_home": elif config[CONF_TYPE] == "armed_home":
to_state = AlarmControlPanelState.ARMED_HOME to_state = STATE_ALARM_ARMED_HOME
elif config[CONF_TYPE] == "armed_away": elif config[CONF_TYPE] == "armed_away":
to_state = AlarmControlPanelState.ARMED_AWAY to_state = STATE_ALARM_ARMED_AWAY
elif config[CONF_TYPE] == "armed_night": elif config[CONF_TYPE] == "armed_night":
to_state = AlarmControlPanelState.ARMED_NIGHT to_state = STATE_ALARM_ARMED_NIGHT
elif config[CONF_TYPE] == "armed_vacation": elif config[CONF_TYPE] == "armed_vacation":
to_state = AlarmControlPanelState.ARMED_VACATION to_state = STATE_ALARM_ARMED_VACATION
state_config = { state_config = {
state_trigger.CONF_PLATFORM: "state", state_trigger.CONF_PLATFORM: "state",

View file

@ -16,21 +16,28 @@ 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, AlarmControlPanelState from . import DOMAIN
_LOGGER: Final = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
VALID_STATES: Final[set[str]] = { VALID_STATES: Final[set[str]] = {
AlarmControlPanelState.ARMED_AWAY, STATE_ALARM_ARMED_AWAY,
AlarmControlPanelState.ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_CUSTOM_BYPASS,
AlarmControlPanelState.ARMED_HOME, STATE_ALARM_ARMED_HOME,
AlarmControlPanelState.ARMED_NIGHT, STATE_ALARM_ARMED_NIGHT,
AlarmControlPanelState.ARMED_VACATION, STATE_ALARM_ARMED_VACATION,
AlarmControlPanelState.DISARMED, STATE_ALARM_DISARMED,
AlarmControlPanelState.TRIGGERED, STATE_ALARM_TRIGGERED,
} }
@ -58,19 +65,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 == AlarmControlPanelState.ARMED_AWAY: if state.state == STATE_ALARM_ARMED_AWAY:
service = SERVICE_ALARM_ARM_AWAY service = SERVICE_ALARM_ARM_AWAY
elif state.state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS: elif state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
service = SERVICE_ALARM_ARM_CUSTOM_BYPASS service = SERVICE_ALARM_ARM_CUSTOM_BYPASS
elif state.state == AlarmControlPanelState.ARMED_HOME: elif state.state == STATE_ALARM_ARMED_HOME:
service = SERVICE_ALARM_ARM_HOME service = SERVICE_ALARM_ARM_HOME
elif state.state == AlarmControlPanelState.ARMED_NIGHT: elif state.state == STATE_ALARM_ARMED_NIGHT:
service = SERVICE_ALARM_ARM_NIGHT service = SERVICE_ALARM_ARM_NIGHT
elif state.state == AlarmControlPanelState.ARMED_VACATION: elif state.state == STATE_ALARM_ARMED_VACATION:
service = SERVICE_ALARM_ARM_VACATION service = SERVICE_ALARM_ARM_VACATION
elif state.state == AlarmControlPanelState.DISARMED: elif state.state == STATE_ALARM_DISARMED:
service = SERVICE_ALARM_DISARM service = SERVICE_ALARM_DISARM
elif state.state == AlarmControlPanelState.TRIGGERED: elif state.state == STATE_ALARM_TRIGGERED:
service = SERVICE_ALARM_TRIGGER service = SERVICE_ALARM_TRIGGER
await hass.services.async_call( await hass.services.async_call(

View file

@ -7,10 +7,16 @@ 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 ATTR_CODE from homeassistant.const import (
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
@ -100,15 +106,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_alarm_state = AlarmControlPanelState.TRIGGERED self._attr_state = STATE_ALARM_TRIGGERED
elif message.armed_away: elif message.armed_away:
self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY self._attr_state = STATE_ALARM_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_alarm_state = AlarmControlPanelState.ARMED_NIGHT self._attr_state = STATE_ALARM_ARMED_NIGHT
elif message.armed_home: elif message.armed_home:
self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME self._attr_state = STATE_ALARM_ARMED_HOME
else: else:
self._attr_alarm_state = AlarmControlPanelState.DISARMED self._attr_state = STATE_ALARM_DISARMED
self._attr_extra_state_attributes = { self._attr_extra_state_attributes = {
"ac_power": message.ac_power, "ac_power": message.ac_power,

View file

@ -26,7 +26,6 @@ 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
@ -37,6 +36,10 @@ 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,
@ -1314,13 +1317,13 @@ class AlexaSecurityPanelController(AlexaCapability):
raise UnsupportedProperty(name) raise UnsupportedProperty(name)
arm_state = self.entity.state arm_state = self.entity.state
if arm_state == AlarmControlPanelState.ARMED_HOME: if arm_state == STATE_ALARM_ARMED_HOME:
return "ARMED_STAY" return "ARMED_STAY"
if arm_state == AlarmControlPanelState.ARMED_AWAY: if arm_state == STATE_ALARM_ARMED_AWAY:
return "ARMED_AWAY" return "ARMED_AWAY"
if arm_state == AlarmControlPanelState.ARMED_NIGHT: if arm_state == STATE_ALARM_ARMED_NIGHT:
return "ARMED_NIGHT" return "ARMED_NIGHT"
if arm_state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS: if arm_state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
return "ARMED_STAY" return "ARMED_STAY"
return "DISARMED" return "DISARMED"

View file

@ -9,7 +9,6 @@ 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,
@ -52,6 +51,7 @@ 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,13 +1083,7 @@ 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}
# Per Alexa Documentation: users are not allowed to switch from armed_away if entity.state != STATE_ALARM_DISARMED:
# 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)
@ -1139,7 +1133,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 == alarm_control_panel.AlarmControlPanelState.DISARMED: if entity.state == STATE_ALARM_DISARMED:
return response return response
payload = directive.payload payload = directive.payload

View file

@ -29,7 +29,6 @@ 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 (
@ -137,7 +136,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 is_hassio(self.hass) return hassio.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", "hassio", "recorder"], "after_dependencies": ["energy", "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

@ -16,6 +16,7 @@ from homeassistant.config_entries import (
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlow,
OptionsFlowWithConfigEntry,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -26,7 +27,6 @@ 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,
@ -45,11 +45,9 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow( def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
config_entry: ConfigEntry,
) -> HomeassistantAnalyticsOptionsFlowHandler:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return HomeassistantAnalyticsOptionsFlowHandler() return HomeassistantAnalyticsOptionsFlowHandler(config_entry)
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
@ -57,12 +55,8 @@ 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 all( if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
[ 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:
@ -70,7 +64,6 @@ 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, []
), ),
@ -84,7 +77,6 @@ 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:
@ -107,13 +99,6 @@ 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,
@ -133,7 +118,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
) )
class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow): class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle Homeassistant Analytics options.""" """Handle Homeassistant Analytics options."""
async def async_step_init( async def async_step_init(
@ -142,19 +127,14 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
"""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 all( if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
[ 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, []
), ),
@ -168,7 +148,6 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
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:
@ -189,13 +168,6 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
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,
@ -212,6 +184,6 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
), ),
}, },
), ),
self.config_entry.options, self.options,
), ),
) )

View file

@ -4,7 +4,6 @@ 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,13 +12,11 @@ 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,
@ -35,7 +33,6 @@ class AnalyticsData:
active_installations: int active_installations: int
reports_integrations: 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]
@ -56,7 +53,6 @@ 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
] ]
@ -66,7 +62,6 @@ 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:
@ -75,9 +70,6 @@ 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
@ -89,19 +81,11 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
return AnalyticsData( return AnalyticsData(
data.active_installations, data.active_installations,
data.reports_integrations, data.reports_integrations,
addons,
core_integrations, core_integrations,
custom_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(
data: dict[str, CustomIntegration], domain: str data: dict[str, CustomIntegration], domain: str
) -> int: ) -> int:

View file

@ -29,20 +29,6 @@ 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:
@ -103,13 +89,6 @@ 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,

View file

@ -3,12 +3,10 @@
"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"
} }
@ -26,12 +24,10 @@
"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%]"
} }

View file

@ -4,7 +4,6 @@ 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
@ -41,7 +40,6 @@ 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,
@ -68,8 +66,6 @@ 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:
@ -161,32 +157,6 @@ 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

@ -13,7 +13,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithConfigEntry,
) )
from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT
from homeassistant.core import callback from homeassistant.core import callback
@ -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_INTERVAL, CONF_SCREENCAP,
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_INTERVAL, DEFAULT_SCREENCAP,
DEVICE_CLASSES, DEVICE_CLASSES,
DOMAIN, DOMAIN,
PROP_ETHMAC, PROP_ETHMAC,
@ -76,7 +76,6 @@ 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(
@ -186,14 +185,16 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN):
return OptionsFlowHandler(config_entry) return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(OptionsFlow): class OptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle an option flow for Android Debug Bridge.""" """Handle an option flow for Android Debug Bridge."""
def __init__(self, config_entry: ConfigEntry) -> None: def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow.""" """Initialize options flow."""
self._apps: dict[str, Any] = dict(config_entry.options.get(CONF_APPS, {})) super().__init__(config_entry)
self._state_det_rules: dict[str, Any] = dict(
config_entry.options.get(CONF_STATE_DETECTION_RULES, {}) self._apps: dict[str, Any] = self.options.setdefault(CONF_APPS, {})
self._state_det_rules: dict[str, Any] = self.options.setdefault(
CONF_STATE_DETECTION_RULES, {}
) )
self._conf_app_id: str | None = None self._conf_app_id: str | None = None
self._conf_rule_id: str | None = None self._conf_rule_id: str | None = None
@ -235,7 +236,7 @@ class OptionsFlowHandler(OptionsFlow):
SelectOptionDict(value=k, label=v) for k, v in apps_list.items() SelectOptionDict(value=k, label=v) for k, v in apps_list.items()
] ]
rules = [RULES_NEW_ID, *self._state_det_rules] rules = [RULES_NEW_ID, *self._state_det_rules]
options = self.config_entry.options options = self.options
data_schema = vol.Schema( data_schema = vol.Schema(
{ {
@ -252,12 +253,10 @@ class OptionsFlowHandler(OptionsFlow):
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
), ),
): bool, ): bool,
vol.Required( vol.Optional(
CONF_SCREENCAP_INTERVAL, CONF_SCREENCAP,
default=options.get( default=options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP),
CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL ): bool,
),
): 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,7 +9,6 @@ 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"
@ -19,7 +18,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_INTERVAL = 5 DEFAULT_SCREENCAP = True
DEVICE_ANDROIDTV = "androidtv" DEVICE_ANDROIDTV = "androidtv"
DEVICE_FIRETV = "firetv" DEVICE_FIRETV = "firetv"

View file

@ -2,9 +2,10 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import 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
@ -22,19 +23,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.dt import utcnow from homeassistant.util import Throttle
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_INTERVAL, CONF_SCREENCAP,
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_INTERVAL, DEFAULT_SCREENCAP,
DEVICE_ANDROIDTV, DEVICE_ANDROIDTV,
SIGNAL_CONFIG_ENTITY, SIGNAL_CONFIG_ENTITY,
) )
@ -47,6 +48,8 @@ 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"
@ -122,8 +125,7 @@ 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_delta: timedelta | None = None self._screencap = DEFAULT_SCREENCAP
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
@ -157,13 +159,7 @@ 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
) )
screencap_interval: int = options.get( self._screencap = options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP)
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)
@ -187,7 +183,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_delta not self._screencap
or self.state in {MediaPlayerState.OFF, None} or self.state in {MediaPlayerState.OFF, None}
or not self.available or not self.available
): ):
@ -197,18 +193,11 @@ 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(force) await self._adb_get_screencap(no_throttle=force)
async def _adb_get_screencap(self, force: bool = False) -> None: @Throttle(MIN_TIME_BETWEEN_SCREENCAPS)
"""Take a screen capture from the device every configured minutes.""" async def _adb_get_screencap(self, **kwargs: Any) -> None:
time_elapsed = self._screencap_delta is not None and ( """Take a screen capture from the device every 60 seconds."""
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_interval": "Interval in minutes between screen capture for album art (set 0 to disable)", "screencap": "Use screen capture for album art",
"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

@ -20,7 +20,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithConfigEntry,
) )
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
@ -221,12 +221,13 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
return AndroidTVRemoteOptionsFlowHandler(config_entry) return AndroidTVRemoteOptionsFlowHandler(config_entry)
class AndroidTVRemoteOptionsFlowHandler(OptionsFlow): class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Android TV Remote options flow.""" """Android TV Remote options flow."""
def __init__(self, config_entry: ConfigEntry) -> None: def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow.""" """Initialize options flow."""
self._apps: dict[str, Any] = dict(config_entry.options.get(CONF_APPS, {})) super().__init__(config_entry)
self._apps: dict[str, Any] = self.options.setdefault(CONF_APPS, {})
self._conf_app_id: str | None = None self._conf_app_id: str | None = None
@callback @callback

View file

@ -13,7 +13,7 @@ from anova_wifi import (
WebsocketFailure, WebsocketFailure,
) )
from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.const import 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,25 +71,3 @@ 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_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_DEVICES, 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,7 +16,6 @@ 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
@ -43,6 +42,8 @@ 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

@ -121,6 +121,7 @@ class AnthropicOptionsFlow(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None: def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow.""" """Initialize options flow."""
self.config_entry = config_entry
self.last_rendered_recommended = config_entry.options.get( self.last_rendered_recommended = config_entry.options.get(
CONF_RECOMMENDED, False CONF_RECOMMENDED, False
) )

View file

@ -15,14 +15,12 @@ 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 .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type AranetConfigEntry = ConfigEntry[
PassiveBluetoothProcessorCoordinator[Aranet4Advertisement]
]
def _service_info_to_adv( def _service_info_to_adv(
service_info: BluetoothServiceInfoBleak, service_info: BluetoothServiceInfoBleak,
@ -30,25 +28,30 @@ def _service_info_to_adv(
return Aranet4Advertisement(service_info.device, service_info.advertisement) return Aranet4Advertisement(service_info.device, service_info.advertisement)
async def async_setup_entry(hass: HomeAssistant, entry: AranetConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Aranet from a config entry.""" """Set up Aranet from a config entry."""
address = entry.unique_id address = entry.unique_id
assert address is not None assert address is not None
coordinator = PassiveBluetoothProcessorCoordinator( coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
hass, PassiveBluetoothProcessorCoordinator(
_LOGGER, hass,
address=address, _LOGGER,
mode=BluetoothScanningMode.PASSIVE, address=address,
update_method=_service_info_to_adv, mode=BluetoothScanningMode.PASSIVE,
update_method=_service_info_to_adv,
)
) )
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# only start after all platforms have had a chance to subscribe entry.async_on_unload(
entry.async_on_unload(coordinator.async_start()) coordinator.async_start()
) # only start after all platforms have had a chance to subscribe
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: AranetConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View file

@ -8,10 +8,12 @@ from typing import Any
from aranet4.client import Aranet4Advertisement from aranet4.client import Aranet4Advertisement
from bleak.backends.device import BLEDevice from bleak.backends.device import BLEDevice
from homeassistant import config_entries
from homeassistant.components.bluetooth.passive_update_processor import ( from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothDataProcessor, PassiveBluetoothDataProcessor,
PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdate,
PassiveBluetoothEntityKey, PassiveBluetoothEntityKey,
PassiveBluetoothProcessorCoordinator,
PassiveBluetoothProcessorEntity, PassiveBluetoothProcessorEntity,
) )
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -36,8 +38,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AranetConfigEntry from .const import ARANET_MANUFACTURER_NAME, DOMAIN
from .const import ARANET_MANUFACTURER_NAME
@dataclass(frozen=True) @dataclass(frozen=True)
@ -173,17 +174,20 @@ def sensor_update_to_bluetooth_data_update(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AranetConfigEntry, entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Aranet sensors.""" """Set up the Aranet sensors."""
coordinator: PassiveBluetoothProcessorCoordinator[Aranet4Advertisement] = hass.data[
DOMAIN
][entry.entry_id]
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
entry.async_on_unload( entry.async_on_unload(
processor.async_add_entities_listener( processor.async_add_entities_listener(
Aranet4BluetoothSensorEntity, async_add_entities Aranet4BluetoothSensorEntity, async_add_entities
) )
) )
entry.async_on_unload(entry.runtime_data.async_register_processor(processor)) entry.async_on_unload(coordinator.async_register_processor(processor))
class Aranet4BluetoothSensorEntity( class Aranet4BluetoothSensorEntity(

View file

@ -22,8 +22,8 @@ class EnhancedAudioChunk:
timestamp_ms: int timestamp_ms: int
"""Timestamp relative to start of audio stream (milliseconds)""" """Timestamp relative to start of audio stream (milliseconds)"""
speech_probability: float | None is_speech: bool | None
"""Probability that audio chunk contains speech (0-1), None if unknown""" """True if audio chunk likely contains speech, False if not, None if unknown"""
class AudioEnhancer(ABC): class AudioEnhancer(ABC):
@ -70,27 +70,27 @@ class MicroVadSpeexEnhancer(AudioEnhancer):
) )
self.vad: MicroVad | None = None self.vad: MicroVad | None = None
self.threshold = 0.5
if self.is_vad_enabled: if self.is_vad_enabled:
self.vad = MicroVad() self.vad = MicroVad()
_LOGGER.debug("Initialized microVAD") _LOGGER.debug("Initialized microVAD with threshold=%s", self.threshold)
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk: def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples.""" """Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
speech_probability: float | None = None is_speech: bool | None = None
assert len(audio) == BYTES_PER_CHUNK assert len(audio) == BYTES_PER_CHUNK
if self.vad is not None: if self.vad is not None:
# Run VAD # Run VAD
speech_probability = self.vad.Process10ms(audio) speech_prob = self.vad.Process10ms(audio)
is_speech = speech_prob > self.threshold
if self.audio_processor is not None: if self.audio_processor is not None:
# Run noise suppression and auto gain # Run noise suppression and auto gain
audio = self.audio_processor.Process10ms(audio).audio audio = self.audio_processor.Process10ms(audio).audio
return EnhancedAudioChunk( return EnhancedAudioChunk(
audio=audio, audio=audio, timestamp_ms=timestamp_ms, is_speech=is_speech
timestamp_ms=timestamp_ms,
speech_probability=speech_probability,
) )

View file

@ -780,9 +780,7 @@ class PipelineRun:
# speaking the voice command. # speaking the voice command.
audio_chunks_for_stt.extend( audio_chunks_for_stt.extend(
EnhancedAudioChunk( EnhancedAudioChunk(
audio=chunk_ts[0], audio=chunk_ts[0], timestamp_ms=chunk_ts[1], is_speech=False
timestamp_ms=chunk_ts[1],
speech_probability=None,
) )
for chunk_ts in result.queued_audio for chunk_ts in result.queued_audio
) )
@ -829,7 +827,7 @@ class PipelineRun:
if wake_word_vad is not None: if wake_word_vad is not None:
chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate
if not wake_word_vad.process(chunk_seconds, chunk.speech_probability): if not wake_word_vad.process(chunk_seconds, chunk.is_speech):
raise WakeWordTimeoutError( raise WakeWordTimeoutError(
code="wake-word-timeout", message="Wake word was not detected" code="wake-word-timeout", message="Wake word was not detected"
) )
@ -957,7 +955,7 @@ class PipelineRun:
if stt_vad is not None: if stt_vad is not None:
chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate chunk_seconds = (len(chunk.audio) // sample_width) / sample_rate
if not stt_vad.process(chunk_seconds, chunk.speech_probability): if not stt_vad.process(chunk_seconds, chunk.is_speech):
# Silence detected at the end of voice command # Silence detected at the end of voice command
self.process_event( self.process_event(
PipelineEvent( PipelineEvent(
@ -1223,7 +1221,7 @@ class PipelineRun:
yield EnhancedAudioChunk( yield EnhancedAudioChunk(
audio=sub_chunk, audio=sub_chunk,
timestamp_ms=timestamp_ms, timestamp_ms=timestamp_ms,
speech_probability=None, # no VAD is_speech=None, # no VAD
) )
timestamp_ms += MS_PER_CHUNK timestamp_ms += MS_PER_CHUNK

View file

@ -75,7 +75,7 @@ class AudioBuffer:
class VoiceCommandSegmenter: class VoiceCommandSegmenter:
"""Segments an audio stream into voice commands.""" """Segments an audio stream into voice commands."""
speech_seconds: float = 0.1 speech_seconds: float = 0.3
"""Seconds of speech before voice command has started.""" """Seconds of speech before voice command has started."""
command_seconds: float = 1.0 command_seconds: float = 1.0
@ -96,12 +96,6 @@ class VoiceCommandSegmenter:
timed_out: bool = False timed_out: bool = False
"""True a timeout occurred during voice command.""" """True a timeout occurred during voice command."""
before_command_speech_threshold: float = 0.2
"""Probability threshold for speech before voice command."""
in_command_speech_threshold: float = 0.5
"""Probability threshold for speech during voice command."""
_speech_seconds_left: float = 0.0 _speech_seconds_left: float = 0.0
"""Seconds left before considering voice command as started.""" """Seconds left before considering voice command as started."""
@ -130,7 +124,7 @@ class VoiceCommandSegmenter:
self._reset_seconds_left = self.reset_seconds self._reset_seconds_left = self.reset_seconds
self.in_command = False self.in_command = False
def process(self, chunk_seconds: float, speech_probability: float | None) -> bool: def process(self, chunk_seconds: float, is_speech: bool | None) -> bool:
"""Process samples using external VAD. """Process samples using external VAD.
Returns False when command is done. Returns False when command is done.
@ -148,12 +142,7 @@ class VoiceCommandSegmenter:
self.timed_out = True self.timed_out = True
return False return False
if speech_probability is None:
speech_probability = 0.0
if not self.in_command: if not self.in_command:
# Before command
is_speech = speech_probability > self.before_command_speech_threshold
if is_speech: if is_speech:
self._reset_seconds_left = self.reset_seconds self._reset_seconds_left = self.reset_seconds
self._speech_seconds_left -= chunk_seconds self._speech_seconds_left -= chunk_seconds
@ -171,29 +160,24 @@ class VoiceCommandSegmenter:
if self._reset_seconds_left <= 0: if self._reset_seconds_left <= 0:
self._speech_seconds_left = self.speech_seconds self._speech_seconds_left = self.speech_seconds
self._reset_seconds_left = self.reset_seconds self._reset_seconds_left = self.reset_seconds
elif not is_speech:
# Silence in command
self._reset_seconds_left = self.reset_seconds
self._silence_seconds_left -= chunk_seconds
self._command_seconds_left -= chunk_seconds
if (self._silence_seconds_left <= 0) and (self._command_seconds_left <= 0):
# Command finished successfully
self.reset()
_LOGGER.debug("Voice command finished")
return False
else: else:
# In command # Speech in command.
is_speech = speech_probability > self.in_command_speech_threshold # Reset silence counter if enough speech.
if not is_speech: self._reset_seconds_left -= chunk_seconds
# Silence in command self._command_seconds_left -= chunk_seconds
if self._reset_seconds_left <= 0:
self._silence_seconds_left = self.silence_seconds
self._reset_seconds_left = self.reset_seconds self._reset_seconds_left = self.reset_seconds
self._silence_seconds_left -= chunk_seconds
self._command_seconds_left -= chunk_seconds
if (self._silence_seconds_left <= 0) and (
self._command_seconds_left <= 0
):
# Command finished successfully
self.reset()
_LOGGER.debug("Voice command finished")
return False
else:
# Speech in command.
# Reset silence counter if enough speech.
self._reset_seconds_left -= chunk_seconds
self._command_seconds_left -= chunk_seconds
if self._reset_seconds_left <= 0:
self._silence_seconds_left = self.silence_seconds
self._reset_seconds_left = self.reset_seconds
return True return True
@ -242,9 +226,6 @@ class VoiceActivityTimeout:
reset_seconds: float = 0.5 reset_seconds: float = 0.5
"""Seconds of speech before resetting timeout.""" """Seconds of speech before resetting timeout."""
speech_threshold: float = 0.5
"""Threshold for speech."""
_silence_seconds_left: float = 0.0 _silence_seconds_left: float = 0.0
"""Seconds left before considering voice command as stopped.""" """Seconds left before considering voice command as stopped."""
@ -260,15 +241,12 @@ class VoiceActivityTimeout:
self._silence_seconds_left = self.silence_seconds self._silence_seconds_left = self.silence_seconds
self._reset_seconds_left = self.reset_seconds self._reset_seconds_left = self.reset_seconds
def process(self, chunk_seconds: float, speech_probability: float | None) -> bool: def process(self, chunk_seconds: float, is_speech: bool | None) -> bool:
"""Process samples using external VAD. """Process samples using external VAD.
Returns False when timeout is reached. Returns False when timeout is reached.
""" """
if speech_probability is None: if is_speech:
speech_probability = 0.0
if speech_probability > self.speech_threshold:
# Speech # Speech
self._reset_seconds_left -= chunk_seconds self._reset_seconds_left -= chunk_seconds
if self._reset_seconds_left <= 0: if self._reset_seconds_left <= 0:

View file

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/autarco", "documentation": "https://www.home-assistant.io/integrations/autarco",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["autarco==3.1.0"] "requirements": ["autarco==3.0.0"]
} }

View file

@ -3,6 +3,11 @@
"name": "Awair", "name": "Awair",
"codeowners": ["@ahayworth", "@danielsjf"], "codeowners": ["@ahayworth", "@danielsjf"],
"config_flow": true, "config_flow": true,
"dhcp": [
{
"macaddress": "70886B1*"
}
],
"documentation": "https://www.home-assistant.io/integrations/awair", "documentation": "https://www.home-assistant.io/integrations/awair",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["python_awair"], "loggers": ["python_awair"],

View file

@ -18,7 +18,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithConfigEntry,
) )
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@ -59,11 +59,9 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow( def async_get_options_flow(config_entry: ConfigEntry) -> AxisOptionsFlowHandler:
config_entry: ConfigEntry,
) -> AxisOptionsFlowHandler:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return AxisOptionsFlowHandler() return AxisOptionsFlowHandler(config_entry)
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the Axis config flow.""" """Initialize the Axis config flow."""
@ -266,7 +264,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
return await self.async_step_user() return await self.async_step_user()
class AxisOptionsFlowHandler(OptionsFlow): class AxisOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle Axis device options.""" """Handle Axis device options."""
config_entry: AxisConfigEntry config_entry: AxisConfigEntry
@ -284,7 +282,8 @@ class AxisOptionsFlowHandler(OptionsFlow):
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Manage the Axis device stream options.""" """Manage the Axis device stream options."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(data=self.config_entry.options | user_input) self.options.update(user_input)
return self.async_create_entry(title="", data=self.options)
schema = {} schema = {}

View file

@ -30,7 +30,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["axis"], "loggers": ["axis"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["axis==63"], "requirements": ["axis==62"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "AXIS" "manufacturer": "AXIS"

View file

@ -124,9 +124,7 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
step_id=STEP_CONN_STRING, step_id=STEP_CONN_STRING,
data_schema=CONN_STRING_SCHEMA, data_schema=CONN_STRING_SCHEMA,
errors=errors, errors=errors,
description_placeholders={ description_placeholders=self._data[CONF_EVENT_HUB_INSTANCE_NAME],
"event_hub_instance_name": self._data[CONF_EVENT_HUB_INSTANCE_NAME]
},
last_step=True, last_step=True,
) )
@ -146,9 +144,7 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
step_id=STEP_SAS, step_id=STEP_SAS,
data_schema=SAS_SCHEMA, data_schema=SAS_SCHEMA,
errors=errors, errors=errors,
description_placeholders={ description_placeholders=self._data[CONF_EVENT_HUB_INSTANCE_NAME],
"event_hub_instance_name": self._data[CONF_EVENT_HUB_INSTANCE_NAME]
},
last_step=True, last_step=True,
) )

View file

@ -1,8 +1,8 @@
"""The Backup integration.""" """The Backup integration."""
from homeassistant.components.hassio import is_hassio
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import DATA_MANAGER, DOMAIN, LOGGER from .const import DATA_MANAGER, DOMAIN, LOGGER
@ -32,9 +32,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_handle_create_service(call: ServiceCall) -> None: async def async_handle_create_service(call: ServiceCall) -> None:
"""Service handler for creating backups.""" """Service handler for creating backups."""
await backup_manager.async_create_backup(on_progress=None) await backup_manager.async_create_backup()
if backup_task := backup_manager.backup_task:
await backup_task
hass.services.async_register(DOMAIN, "create", async_handle_create_service) hass.services.async_register(DOMAIN, "create", async_handle_create_service)

View file

@ -17,7 +17,6 @@ LOGGER = getLogger(__package__)
EXCLUDE_FROM_BACKUP = [ EXCLUDE_FROM_BACKUP = [
"__pycache__/*", "__pycache__/*",
".DS_Store", ".DS_Store",
".HA_RESTORE",
"*.db-shm", "*.db-shm",
"*.log.*", "*.log.*",
"*.log", "*.log",

View file

@ -2,26 +2,23 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from http import HTTPStatus from http import HTTPStatus
from typing import cast
from aiohttp import BodyPartReader
from aiohttp.hdrs import CONTENT_DISPOSITION from aiohttp.hdrs import CONTENT_DISPOSITION
from aiohttp.web import FileResponse, Request, Response from aiohttp.web import FileResponse, Request, Response
from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.util import slugify from homeassistant.util import slugify
from .const import DATA_MANAGER from .const import DOMAIN
from .manager import BaseBackupManager
@callback @callback
def async_register_http_views(hass: HomeAssistant) -> None: def async_register_http_views(hass: HomeAssistant) -> None:
"""Register the http views.""" """Register the http views."""
hass.http.register_view(DownloadBackupView) hass.http.register_view(DownloadBackupView)
hass.http.register_view(UploadBackupView)
class DownloadBackupView(HomeAssistantView): class DownloadBackupView(HomeAssistantView):
@ -39,7 +36,7 @@ class DownloadBackupView(HomeAssistantView):
if not request["hass_user"].is_admin: if not request["hass_user"].is_admin:
return Response(status=HTTPStatus.UNAUTHORIZED) return Response(status=HTTPStatus.UNAUTHORIZED)
manager = request.app[KEY_HASS].data[DATA_MANAGER] manager: BaseBackupManager = request.app[KEY_HASS].data[DOMAIN]
backup = await manager.async_get_backup(slug=slug) backup = await manager.async_get_backup(slug=slug)
if backup is None or not backup.path.exists(): if backup is None or not backup.path.exists():
@ -51,29 +48,3 @@ class DownloadBackupView(HomeAssistantView):
CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar" CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar"
}, },
) )
class UploadBackupView(HomeAssistantView):
"""Generate backup view."""
url = "/api/backup/upload"
name = "api:backup:upload"
@require_admin
async def post(self, request: Request) -> Response:
"""Upload a backup file."""
manager = request.app[KEY_HASS].data[DATA_MANAGER]
reader = await request.multipart()
contents = cast(BodyPartReader, await reader.next())
try:
await manager.async_receive_backup(contents=contents)
except OSError as err:
return Response(
body=f"Can't write backup file {err}",
status=HTTPStatus.INTERNAL_SERVER_ERROR,
)
except asyncio.CancelledError:
return Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)
return Response(status=HTTPStatus.CREATED)

View file

@ -4,24 +4,18 @@ from __future__ import annotations
import abc import abc
import asyncio import asyncio
from collections.abc import Callable
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
import hashlib import hashlib
import io import io
import json import json
from pathlib import Path from pathlib import Path
from queue import SimpleQueue
import shutil
import tarfile import tarfile
from tarfile import TarError from tarfile import TarError
from tempfile import TemporaryDirectory
import time import time
from typing import Any, Protocol, cast from typing import Any, Protocol, cast
import aiohttp
from securetar import SecureTarFile, atomic_contents_add from securetar import SecureTarFile, atomic_contents_add
from homeassistant.backup_restore import RESTORE_BACKUP_FILE
from homeassistant.const import __version__ as HAVERSION from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -35,13 +29,6 @@ from .const import DOMAIN, EXCLUDE_FROM_BACKUP, LOGGER
BUF_SIZE = 2**20 * 4 # 4MB BUF_SIZE = 2**20 * 4 # 4MB
@dataclass(slots=True)
class NewBackup:
"""New backup class."""
slug: str
@dataclass(slots=True) @dataclass(slots=True)
class Backup: class Backup:
"""Backup class.""" """Backup class."""
@ -57,15 +44,6 @@ class Backup:
return {**asdict(self), "path": self.path.as_posix()} return {**asdict(self), "path": self.path.as_posix()}
@dataclass(slots=True)
class BackupProgress:
"""Backup progress class."""
done: bool
stage: str | None
success: bool | None
class BackupPlatformProtocol(Protocol): class BackupPlatformProtocol(Protocol):
"""Define the format that backup platforms can have.""" """Define the format that backup platforms can have."""
@ -82,7 +60,7 @@ class BaseBackupManager(abc.ABC):
def __init__(self, hass: HomeAssistant) -> None: def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the backup manager.""" """Initialize the backup manager."""
self.hass = hass self.hass = hass
self.backup_task: asyncio.Task | None = None self.backing_up = False
self.backups: dict[str, Backup] = {} self.backups: dict[str, Backup] = {}
self.loaded_platforms = False self.loaded_platforms = False
self.platforms: dict[str, BackupPlatformProtocol] = {} self.platforms: dict[str, BackupPlatformProtocol] = {}
@ -146,16 +124,7 @@ class BaseBackupManager(abc.ABC):
self.loaded_platforms = True self.loaded_platforms = True
@abc.abstractmethod @abc.abstractmethod
async def async_restore_backup(self, slug: str, **kwargs: Any) -> None: async def async_create_backup(self, **kwargs: Any) -> Backup:
"""Restore a backup."""
@abc.abstractmethod
async def async_create_backup(
self,
*,
on_progress: Callable[[BackupProgress], None] | None,
**kwargs: Any,
) -> NewBackup:
"""Generate a backup.""" """Generate a backup."""
@abc.abstractmethod @abc.abstractmethod
@ -173,15 +142,6 @@ class BaseBackupManager(abc.ABC):
async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None: async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None:
"""Remove a backup.""" """Remove a backup."""
@abc.abstractmethod
async def async_receive_backup(
self,
*,
contents: aiohttp.BodyPartReader,
**kwargs: Any,
) -> None:
"""Receive and store a backup file from upload."""
class BackupManager(BaseBackupManager): class BackupManager(BaseBackupManager):
"""Backup manager for the Backup integration.""" """Backup manager for the Backup integration."""
@ -257,93 +217,17 @@ class BackupManager(BaseBackupManager):
LOGGER.debug("Removed backup located at %s", backup.path) LOGGER.debug("Removed backup located at %s", backup.path)
self.backups.pop(slug) self.backups.pop(slug)
async def async_receive_backup( async def async_create_backup(self, **kwargs: Any) -> Backup:
self,
*,
contents: aiohttp.BodyPartReader,
**kwargs: Any,
) -> None:
"""Receive and store a backup file from upload."""
queue: SimpleQueue[tuple[bytes, asyncio.Future[None] | None] | None] = (
SimpleQueue()
)
temp_dir_handler = await self.hass.async_add_executor_job(TemporaryDirectory)
target_temp_file = Path(
temp_dir_handler.name, contents.filename or "backup.tar"
)
def _sync_queue_consumer() -> None:
with target_temp_file.open("wb") as file_handle:
while True:
if (_chunk_future := queue.get()) is None:
break
_chunk, _future = _chunk_future
if _future is not None:
self.hass.loop.call_soon_threadsafe(_future.set_result, None)
file_handle.write(_chunk)
fut: asyncio.Future[None] | None = None
try:
fut = self.hass.async_add_executor_job(_sync_queue_consumer)
megabytes_sending = 0
while chunk := await contents.read_chunk(BUF_SIZE):
megabytes_sending += 1
if megabytes_sending % 5 != 0:
queue.put_nowait((chunk, None))
continue
chunk_future = self.hass.loop.create_future()
queue.put_nowait((chunk, chunk_future))
await asyncio.wait(
(fut, chunk_future),
return_when=asyncio.FIRST_COMPLETED,
)
if fut.done():
# The executor job failed
break
queue.put_nowait(None) # terminate queue consumer
finally:
if fut is not None:
await fut
def _move_and_cleanup() -> None:
shutil.move(target_temp_file, self.backup_dir / target_temp_file.name)
temp_dir_handler.cleanup()
await self.hass.async_add_executor_job(_move_and_cleanup)
await self.load_backups()
async def async_create_backup(
self,
*,
on_progress: Callable[[BackupProgress], None] | None,
**kwargs: Any,
) -> NewBackup:
"""Generate a backup.""" """Generate a backup."""
if self.backup_task: if self.backing_up:
raise HomeAssistantError("Backup already in progress") raise HomeAssistantError("Backup already in progress")
backup_name = f"Core {HAVERSION}"
date_str = dt_util.now().isoformat()
slug = _generate_slug(date_str, backup_name)
self.backup_task = self.hass.async_create_task(
self._async_create_backup(backup_name, date_str, slug, on_progress),
name="backup_manager_create_backup",
eager_start=False, # To ensure the task is not started before we return
)
return NewBackup(slug=slug)
async def _async_create_backup(
self,
backup_name: str,
date_str: str,
slug: str,
on_progress: Callable[[BackupProgress], None] | None,
) -> Backup:
"""Generate a backup."""
success = False
try: try:
self.backing_up = True
await self.async_pre_backup_actions() await self.async_pre_backup_actions()
backup_name = f"Core {HAVERSION}"
date_str = dt_util.now().isoformat()
slug = _generate_slug(date_str, backup_name)
backup_data = { backup_data = {
"slug": slug, "slug": slug,
@ -370,12 +254,9 @@ class BackupManager(BaseBackupManager):
if self.loaded_backups: if self.loaded_backups:
self.backups[slug] = backup self.backups[slug] = backup
LOGGER.debug("Generated new backup with slug %s", slug) LOGGER.debug("Generated new backup with slug %s", slug)
success = True
return backup return backup
finally: finally:
if on_progress: self.backing_up = False
on_progress(BackupProgress(done=True, stage=None, success=success))
self.backup_task = None
await self.async_post_backup_actions() await self.async_post_backup_actions()
def _mkdir_and_generate_backup_contents( def _mkdir_and_generate_backup_contents(
@ -410,25 +291,6 @@ class BackupManager(BaseBackupManager):
return tar_file_path.stat().st_size return tar_file_path.stat().st_size
async def async_restore_backup(self, slug: str, **kwargs: Any) -> None:
"""Restore a backup.
This will write the restore information to .HA_RESTORE which
will be handled during startup by the restore_backup module.
"""
if (backup := await self.async_get_backup(slug=slug)) is None:
raise HomeAssistantError(f"Backup {slug} not found")
def _write_restore_file() -> None:
"""Write the restore file."""
Path(self.hass.config.path(RESTORE_BACKUP_FILE)).write_text(
json.dumps({"path": backup.path.as_posix()}),
encoding="utf-8",
)
await self.hass.async_add_executor_job(_write_restore_file)
await self.hass.services.async_call("homeassistant", "restart", {})
def _generate_slug(date: str, name: str) -> str: def _generate_slug(date: str, name: str) -> str:
"""Generate a backup slug.""" """Generate a backup slug."""

View file

@ -8,7 +8,6 @@ from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from .const import DATA_MANAGER, LOGGER from .const import DATA_MANAGER, LOGGER
from .manager import BackupProgress
@callback @callback
@ -23,7 +22,6 @@ def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) ->
websocket_api.async_register_command(hass, handle_info) websocket_api.async_register_command(hass, handle_info)
websocket_api.async_register_command(hass, handle_create) websocket_api.async_register_command(hass, handle_create)
websocket_api.async_register_command(hass, handle_remove) websocket_api.async_register_command(hass, handle_remove)
websocket_api.async_register_command(hass, handle_restore)
@websocket_api.require_admin @websocket_api.require_admin
@ -41,7 +39,7 @@ async def handle_info(
msg["id"], msg["id"],
{ {
"backups": list(backups.values()), "backups": list(backups.values()),
"backing_up": manager.backup_task is not None, "backing_up": manager.backing_up,
}, },
) )
@ -87,24 +85,6 @@ async def handle_remove(
connection.send_result(msg["id"]) connection.send_result(msg["id"])
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "backup/restore",
vol.Required("slug"): str,
}
)
@websocket_api.async_response
async def handle_restore(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Restore a backup."""
await hass.data[DATA_MANAGER].async_restore_backup(msg["slug"])
connection.send_result(msg["id"])
@websocket_api.require_admin @websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "backup/generate"}) @websocket_api.websocket_command({vol.Required("type"): "backup/generate"})
@websocket_api.async_response @websocket_api.async_response
@ -114,11 +94,7 @@ async def handle_create(
msg: dict[str, Any], msg: dict[str, Any],
) -> None: ) -> None:
"""Generate a backup.""" """Generate a backup."""
backup = await hass.data[DATA_MANAGER].async_create_backup()
def on_progress(progress: BackupProgress) -> None:
connection.send_message(websocket_api.event_message(msg["id"], progress))
backup = await hass.data[DATA_MANAGER].async_create_backup(on_progress=on_progress)
connection.send_result(msg["id"], backup) connection.send_result(msg["id"], backup)
@ -132,6 +108,7 @@ async def handle_backup_start(
) -> None: ) -> None:
"""Backup start notification.""" """Backup start notification."""
manager = hass.data[DATA_MANAGER] manager = hass.data[DATA_MANAGER]
manager.backing_up = True
LOGGER.debug("Backup start notification") LOGGER.debug("Backup start notification")
try: try:
@ -153,6 +130,7 @@ async def handle_backup_end(
) -> None: ) -> None:
"""Backup end notification.""" """Backup end notification."""
manager = hass.data[DATA_MANAGER] manager = hass.data[DATA_MANAGER]
manager.backing_up = False
LOGGER.debug("Backup end notification") LOGGER.debug("Backup end notification")
try: try:

View file

@ -14,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .const import CONF_SYNC_TIME, DEFAULT_SYNC_TIME from .const import CONF_SYNC_TIME, DEFAULT_SYNC_TIME, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,10 +30,8 @@ PLATFORMS = [
KEEP_ALIVE_INTERVAL = timedelta(minutes=1) KEEP_ALIVE_INTERVAL = timedelta(minutes=1)
SYNC_TIME_INTERVAL = timedelta(hours=1) SYNC_TIME_INTERVAL = timedelta(hours=1)
type BalboaConfigEntry = ConfigEntry[SpaClient]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: BalboaConfigEntry) -> bool:
"""Set up Balboa Spa from a config entry.""" """Set up Balboa Spa from a config entry."""
host = entry.data[CONF_HOST] host = entry.data[CONF_HOST]
@ -46,34 +44,41 @@ async def async_setup_entry(hass: HomeAssistant, entry: BalboaConfigEntry) -> bo
_LOGGER.error("Failed to get spa info at %s", host) _LOGGER.error("Failed to get spa info at %s", host)
raise ConfigEntryNotReady("Unable to configure") raise ConfigEntryNotReady("Unable to configure")
entry.runtime_data = spa hass.data.setdefault(DOMAIN, {})[entry.entry_id] = spa
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await async_setup_time_sync(hass, entry) await async_setup_time_sync(hass, entry)
entry.async_on_unload(entry.add_update_listener(update_listener)) entry.async_on_unload(entry.add_update_listener(update_listener))
entry.async_on_unload(spa.disconnect)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: BalboaConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) _LOGGER.debug("Disconnecting from spa")
spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
await spa.disconnect()
return unload_ok
async def update_listener(hass: HomeAssistant, entry: BalboaConfigEntry) -> None: async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update.""" """Handle options update."""
await hass.config_entries.async_reload(entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)
async def async_setup_time_sync(hass: HomeAssistant, entry: BalboaConfigEntry) -> None: async def async_setup_time_sync(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Set up the time sync.""" """Set up the time sync."""
if not entry.options.get(CONF_SYNC_TIME, DEFAULT_SYNC_TIME): if not entry.options.get(CONF_SYNC_TIME, DEFAULT_SYNC_TIME):
return return
_LOGGER.debug("Setting up daily time sync") _LOGGER.debug("Setting up daily time sync")
spa = entry.runtime_data spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
async def sync_time(now: datetime) -> None: async def sync_time(now: datetime) -> None:
now = dt_util.as_local(now) now = dt_util.as_local(now)

View file

@ -12,20 +12,19 @@ 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 . import BalboaConfigEntry from .const import DOMAIN
from .entity import BalboaEntity from .entity import BalboaEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
entry: BalboaConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the spa's binary sensors.""" """Set up the spa's binary sensors."""
spa = entry.runtime_data spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
entities = [ entities = [
BalboaBinarySensorEntity(spa, description) BalboaBinarySensorEntity(spa, description)
for description in BINARY_SENSOR_DESCRIPTIONS for description in BINARY_SENSOR_DESCRIPTIONS

View file

@ -14,6 +14,7 @@ from homeassistant.components.climate import (
HVACAction, HVACAction,
HVACMode, HVACMode,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
PRECISION_HALVES, PRECISION_HALVES,
@ -23,7 +24,6 @@ from homeassistant.const import (
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 . import BalboaConfigEntry
from .const import DOMAIN from .const import DOMAIN
from .entity import BalboaEntity from .entity import BalboaEntity
@ -45,12 +45,10 @@ TEMPERATURE_UNIT_MAP = {
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
entry: BalboaConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the spa climate entity.""" """Set up the spa climate entity."""
async_add_entities([BalboaClimateEntity(entry.runtime_data)]) async_add_entities([BalboaClimateEntity(hass.data[DOMAIN][entry.entry_id])])
class BalboaClimateEntity(BalboaEntity, ClimateEntity): class BalboaClimateEntity(BalboaEntity, ClimateEntity):

View file

@ -5,10 +5,11 @@ from __future__ import annotations
import math import math
from typing import Any, cast from typing import Any, cast
from pybalboa import SpaControl from pybalboa import SpaClient, SpaControl
from pybalboa.enums import OffOnState, UnknownState from pybalboa.enums import OffOnState, UnknownState
from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.components.fan import FanEntity, FanEntityFeature
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 homeassistant.util.percentage import ( from homeassistant.util.percentage import (
@ -16,17 +17,15 @@ from homeassistant.util.percentage import (
ranged_value_to_percentage, ranged_value_to_percentage,
) )
from . import BalboaConfigEntry from .const import DOMAIN
from .entity import BalboaEntity from .entity import BalboaEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
entry: BalboaConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the spa's pumps.""" """Set up the spa's pumps."""
spa = entry.runtime_data spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
async_add_entities(BalboaPumpFanEntity(control) for control in spa.pumps) async_add_entities(BalboaPumpFanEntity(control) for control in spa.pumps)

View file

@ -4,24 +4,23 @@ from __future__ import annotations
from typing import Any, cast from typing import Any, cast
from pybalboa import SpaControl from pybalboa import SpaClient, SpaControl
from pybalboa.enums import OffOnState, UnknownState from pybalboa.enums import OffOnState, UnknownState
from homeassistant.components.light import ColorMode, LightEntity from homeassistant.components.light import ColorMode, LightEntity
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 . import BalboaConfigEntry from .const import DOMAIN
from .entity import BalboaEntity from .entity import BalboaEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
entry: BalboaConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the spa's lights.""" """Set up the spa's lights."""
spa = entry.runtime_data spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
async_add_entities(BalboaLightEntity(control) for control in spa.lights) async_add_entities(BalboaLightEntity(control) for control in spa.lights)

View file

@ -1,23 +1,22 @@
"""Support for Spa Client selects.""" """Support for Spa Client selects."""
from pybalboa import SpaControl from pybalboa import SpaClient, SpaControl
from pybalboa.enums import LowHighRange from pybalboa.enums import LowHighRange
from homeassistant.components.select import SelectEntity from homeassistant.components.select import SelectEntity
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 . import BalboaConfigEntry from .const import DOMAIN
from .entity import BalboaEntity from .entity import BalboaEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
entry: BalboaConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the spa select entity.""" """Set up the spa select entity."""
spa = entry.runtime_data spa: SpaClient = hass.data[DOMAIN][entry.entry_id]
async_add_entities([BalboaTempRangeSelectEntity(spa.temperature_range)]) async_add_entities([BalboaTempRangeSelectEntity(spa.temperature_range)])

View file

@ -31,12 +31,10 @@ class BangOlufsenData:
client: MozartClient client: MozartClient
type BangOlufsenConfigEntry = ConfigEntry[BangOlufsenData]
PLATFORMS = [Platform.MEDIA_PLAYER] PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry.""" """Set up from a config entry."""
# Remove casts to str # Remove casts to str
@ -69,7 +67,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry)
websocket = BangOlufsenWebsocket(hass, entry, client) websocket = BangOlufsenWebsocket(hass, entry, client)
# Add the websocket and API client # Add the websocket and API client
entry.runtime_data = BangOlufsenData(websocket, client) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BangOlufsenData(
websocket,
client,
)
# Start WebSocket connection # Start WebSocket connection
await client.connect_notifications(remote_control=True, reconnect=True) await client.connect_notifications(remote_control=True, reconnect=True)
@ -79,12 +80,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry)
return True return True
async def async_unload_entry( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass: HomeAssistant, entry: BangOlufsenConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
# Close the API client and WebSocket notification listener # Close the API client and WebSocket notification listener
entry.runtime_data.client.disconnect_notifications() hass.data[DOMAIN][entry.entry_id].client.disconnect_notifications()
await entry.runtime_data.client.close_api_client() await hass.data[DOMAIN][entry.entry_id].client.close_api_client()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View file

@ -7,19 +7,20 @@ from typing import Final
from mozart_api.models import Source, SourceArray, SourceTypeEnum from mozart_api.models import Source, SourceArray, SourceTypeEnum
from homeassistant.components.media_player import ( from homeassistant.components.media_player import MediaPlayerState, MediaType
MediaPlayerState,
MediaType,
RepeatMode,
)
class BangOlufsenSource: class BangOlufsenSource:
"""Class used for associating device source ids with friendly names. May not include all sources.""" """Class used for associating device source ids with friendly names. May not include all sources."""
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
BLUETOOTH: Final[Source] = Source(name="Bluetooth", id="bluetooth")
CHROMECAST: Final[Source] = Source(name="Chromecast built-in", id="chromeCast")
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn") LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
SPDIF: Final[Source] = Source(name="Optical", id="spdif") SPDIF: Final[Source] = Source(name="Optical", id="spdif")
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer") NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = { BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
@ -35,17 +36,6 @@ BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
"unknown": MediaPlayerState.IDLE, "unknown": MediaPlayerState.IDLE,
} }
# Dict used for translating Home Assistant settings to device repeat settings.
BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
RepeatMode.ALL: "all",
RepeatMode.ONE: "track",
RepeatMode.OFF: "none",
}
# Dict used for translating device repeat settings to Home Assistant settings.
BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
}
# Media types for play_media # Media types for play_media
class BangOlufsenMediaType(StrEnum): class BangOlufsenMediaType(StrEnum):
@ -133,6 +123,20 @@ VALID_MEDIA_TYPES: Final[tuple] = (
MediaType.CHANNEL, MediaType.CHANNEL,
) )
# Sources on the device that should not be selectable by the user
HIDDEN_SOURCE_IDS: Final[tuple] = (
"airPlay",
"bluetooth",
"chromeCast",
"generator",
"local",
"dlna",
"qplay",
"wpl",
"pl",
"beolink",
"usbIn",
)
# Fallback sources to use in case of API failure. # Fallback sources to use in case of API failure.
FALLBACK_SOURCES: Final[SourceArray] = SourceArray( FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
@ -140,26 +144,23 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
Source( Source(
id="uriStreamer", id="uriStreamer",
is_enabled=True, is_enabled=True,
is_playable=True, is_playable=False,
name="Audio Streamer", name="Audio Streamer",
type=SourceTypeEnum(value="uriStreamer"), type=SourceTypeEnum(value="uriStreamer"),
is_seekable=False,
), ),
Source( Source(
id="bluetooth", id="bluetooth",
is_enabled=True, is_enabled=True,
is_playable=True, is_playable=False,
name="Bluetooth", name="Bluetooth",
type=SourceTypeEnum(value="bluetooth"), type=SourceTypeEnum(value="bluetooth"),
is_seekable=False,
), ),
Source( Source(
id="spotify", id="spotify",
is_enabled=True, is_enabled=True,
is_playable=True, is_playable=False,
name="Spotify Connect", name="Spotify Connect",
type=SourceTypeEnum(value="spotify"), type=SourceTypeEnum(value="spotify"),
is_seekable=True,
), ),
Source( Source(
id="lineIn", id="lineIn",
@ -167,7 +168,6 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True, is_playable=True,
name="Line-In", name="Line-In",
type=SourceTypeEnum(value="lineIn"), type=SourceTypeEnum(value="lineIn"),
is_seekable=False,
), ),
Source( Source(
id="spdif", id="spdif",
@ -175,7 +175,6 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True, is_playable=True,
name="Optical", name="Optical",
type=SourceTypeEnum(value="spdif"), type=SourceTypeEnum(value="spdif"),
is_seekable=False,
), ),
Source( Source(
id="netRadio", id="netRadio",
@ -183,7 +182,6 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True, is_playable=True,
name="B&O Radio", name="B&O Radio",
type=SourceTypeEnum(value="netRadio"), type=SourceTypeEnum(value="netRadio"),
is_seekable=False,
), ),
Source( Source(
id="deezer", id="deezer",
@ -191,7 +189,6 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True, is_playable=True,
name="Deezer", name="Deezer",
type=SourceTypeEnum(value="deezer"), type=SourceTypeEnum(value="deezer"),
is_seekable=True,
), ),
Source( Source(
id="tidalConnect", id="tidalConnect",
@ -199,7 +196,6 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
is_playable=True, is_playable=True,
name="Tidal Connect", name="Tidal Connect",
type=SourceTypeEnum(value="tidalConnect"), type=SourceTypeEnum(value="tidalConnect"),
is_seekable=True,
), ),
] ]
) )

View file

@ -1,9 +0,0 @@
{
"services": {
"beolink_join": { "service": "mdi:location-enter" },
"beolink_expand": { "service": "mdi:location-enter" },
"beolink_unexpand": { "service": "mdi:location-exit" },
"beolink_leave": { "service": "mdi:close-circle-outline" },
"beolink_allstandby": { "service": "mdi:close-circle-multiple-outline" }
}
}

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