mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
Merge branch 'dev' into request_host_fix
This commit is contained in:
commit
081d0a0cdd
@ -6,6 +6,7 @@ core: &core
|
|||||||
- homeassistant/helpers/**
|
- homeassistant/helpers/**
|
||||||
- homeassistant/package_constraints.txt
|
- homeassistant/package_constraints.txt
|
||||||
- homeassistant/util/**
|
- homeassistant/util/**
|
||||||
|
- mypy.ini
|
||||||
- pyproject.toml
|
- pyproject.toml
|
||||||
- requirements.txt
|
- requirements.txt
|
||||||
- setup.cfg
|
- setup.cfg
|
||||||
@ -131,6 +132,7 @@ tests: &tests
|
|||||||
- tests/components/conftest.py
|
- tests/components/conftest.py
|
||||||
- tests/components/diagnostics/**
|
- tests/components/diagnostics/**
|
||||||
- tests/components/history/**
|
- tests/components/history/**
|
||||||
|
- tests/components/light/common.py
|
||||||
- tests/components/logbook/**
|
- tests/components/logbook/**
|
||||||
- tests/components/recorder/**
|
- tests/components/recorder/**
|
||||||
- tests/components/repairs/**
|
- tests/components/repairs/**
|
||||||
|
@ -62,7 +62,7 @@
|
|||||||
"json.schemas": [
|
"json.schemas": [
|
||||||
{
|
{
|
||||||
"fileMatch": ["homeassistant/components/*/manifest.json"],
|
"fileMatch": ["homeassistant/components/*/manifest.json"],
|
||||||
"url": "./script/json_schemas/manifest_schema.json"
|
"url": "${containerWorkspaceFolder}/script/json_schemas/manifest_schema.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
11
.gitattributes
vendored
11
.gitattributes
vendored
@ -11,3 +11,14 @@
|
|||||||
*.pcm binary
|
*.pcm binary
|
||||||
|
|
||||||
Dockerfile.dev linguist-language=Dockerfile
|
Dockerfile.dev linguist-language=Dockerfile
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
CODEOWNERS linguist-generated=true
|
||||||
|
Dockerfile linguist-generated=true
|
||||||
|
homeassistant/generated/*.py linguist-generated=true
|
||||||
|
mypy.ini linguist-generated=true
|
||||||
|
requirements.txt linguist-generated=true
|
||||||
|
requirements_all.txt linguist-generated=true
|
||||||
|
requirements_test_all.txt linguist-generated=true
|
||||||
|
requirements_test_pre_commit.txt linguist-generated=true
|
||||||
|
script/hassfest/docker/Dockerfile linguist-generated=true
|
||||||
|
14
.github/workflows/builder.yml
vendored
14
.github/workflows/builder.yml
vendored
@ -69,7 +69,7 @@ jobs:
|
|||||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
@ -94,7 +94,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: dawidd6/action-download-artifact@v6
|
uses: dawidd6/action-download-artifact@v8
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
repo: home-assistant/frontend
|
repo: home-assistant/frontend
|
||||||
@ -105,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download nightly wheels of intents
|
- name: Download nightly wheels of intents
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: dawidd6/action-download-artifact@v6
|
uses: dawidd6/action-download-artifact@v8
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
repo: home-assistant/intents-package
|
repo: home-assistant/intents-package
|
||||||
@ -509,7 +509,7 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
|
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
|
||||||
with:
|
with:
|
||||||
context: . # So action will not pull the repository again
|
context: . # So action will not pull the repository again
|
||||||
file: ./script/hassfest/docker/Dockerfile
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
@ -517,12 +517,12 @@ jobs:
|
|||||||
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
||||||
|
|
||||||
- name: Run hassfest against core
|
- name: Run hassfest against core
|
||||||
run: docker run --rm -v ${{ github.workspace }}/homeassistant:/github/workspace/homeassistant ${{ env.HASSFEST_IMAGE_TAG }} --core-integrations-path=/github/workspace/homeassistant/components
|
run: docker run --rm -v ${{ github.workspace }}:/github/workspace ${{ env.HASSFEST_IMAGE_TAG }} --core-path=/github/workspace
|
||||||
|
|
||||||
- name: Push Docker image
|
- name: Push Docker image
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
|
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
|
||||||
with:
|
with:
|
||||||
context: . # So action will not pull the repository again
|
context: . # So action will not pull the repository again
|
||||||
file: ./script/hassfest/docker/Dockerfile
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
@ -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@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0
|
||||||
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 }}
|
||||||
|
101
.github/workflows/ci.yaml
vendored
101
.github/workflows/ci.yaml
vendored
@ -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: "2025.2"
|
||||||
DEFAULT_PYTHON: "3.12"
|
DEFAULT_PYTHON: "3.13"
|
||||||
ALL_PYTHON_VERSIONS: "['3.12', '3.13']"
|
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||||
# 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
|
||||||
@ -240,7 +240,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
@ -286,7 +286,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -326,7 +326,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -366,7 +366,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -482,16 +482,15 @@ 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.2.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
lookup-only: 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: 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.2.0
|
||||||
with:
|
with:
|
||||||
path: ${{ env.UV_CACHE_DIR }}
|
path: ${{ env.UV_CACHE_DIR }}
|
||||||
key: >-
|
key: >-
|
||||||
@ -531,6 +530,26 @@ jobs:
|
|||||||
python -m script.gen_requirements_all ci
|
python -m script.gen_requirements_all ci
|
||||||
uv pip install -r requirements_all_pytest.txt -r requirements_test.txt
|
uv pip install -r requirements_all_pytest.txt -r requirements_test.txt
|
||||||
uv pip install -e . --config-settings editable_mode=compat
|
uv pip install -e . --config-settings editable_mode=compat
|
||||||
|
- name: Dump pip freeze
|
||||||
|
run: |
|
||||||
|
python -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
python --version
|
||||||
|
uv pip freeze >> pip_freeze.txt
|
||||||
|
- name: Upload pip_freeze artifact
|
||||||
|
uses: actions/upload-artifact@v4.6.0
|
||||||
|
with:
|
||||||
|
name: pip-freeze-${{ matrix.python-version }}
|
||||||
|
path: pip_freeze.txt
|
||||||
|
overwrite: true
|
||||||
|
- name: Remove pip_freeze
|
||||||
|
run: rm pip_freeze.txt
|
||||||
|
- name: Remove generated requirements_all
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: rm requirements_all_pytest.txt requirements_all_wheels_*.txt
|
||||||
|
- name: Check dirty
|
||||||
|
run: |
|
||||||
|
./script/check_dirty
|
||||||
|
|
||||||
hassfest:
|
hassfest:
|
||||||
name: Check hassfest
|
name: Check hassfest
|
||||||
@ -559,7 +578,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -592,7 +611,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -630,7 +649,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -642,7 +661,7 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json
|
python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json
|
||||||
- name: Upload licenses
|
- name: Upload licenses
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
||||||
path: licenses-${{ matrix.python-version }}.json
|
path: licenses-${{ matrix.python-version }}.json
|
||||||
@ -673,7 +692,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -720,7 +739,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -772,7 +791,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.2.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -780,7 +799,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.2.0
|
||||||
with:
|
with:
|
||||||
path: .mypy_cache
|
path: .mypy_cache
|
||||||
key: >-
|
key: >-
|
||||||
@ -819,6 +838,12 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
- base
|
- base
|
||||||
|
- gen-requirements-all
|
||||||
|
- hassfest
|
||||||
|
- lint-other
|
||||||
|
- lint-ruff
|
||||||
|
- lint-ruff-format
|
||||||
|
- mypy
|
||||||
name: Split tests for full run
|
name: Split tests for full run
|
||||||
steps:
|
steps:
|
||||||
- name: Install additional OS dependencies
|
- name: Install additional OS dependencies
|
||||||
@ -840,7 +865,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -852,7 +877,7 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
||||||
- name: Upload pytest_buckets
|
- name: Upload pytest_buckets
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets
|
||||||
path: pytest_buckets.txt
|
path: pytest_buckets.txt
|
||||||
@ -904,7 +929,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -954,14 +979,14 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: pytest-*.txt
|
path: pytest-*.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
@ -1025,7 +1050,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -1081,7 +1106,7 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.mariadb }}
|
steps.pytest-partial.outputs.mariadb }}
|
||||||
@ -1089,7 +1114,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{
|
name: coverage-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.mariadb }}
|
steps.pytest-partial.outputs.mariadb }}
|
||||||
@ -1154,7 +1179,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -1211,7 +1236,7 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.postgresql }}
|
steps.pytest-partial.outputs.postgresql }}
|
||||||
@ -1219,7 +1244,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{
|
name: coverage-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.postgresql }}
|
steps.pytest-partial.outputs.postgresql }}
|
||||||
@ -1248,7 +1273,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'true'
|
if: needs.info.outputs.test_full_suite == 'true'
|
||||||
uses: codecov/codecov-action@v5.0.7
|
uses: codecov/codecov-action@v5.3.0
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
flags: full-suite
|
flags: full-suite
|
||||||
@ -1300,7 +1325,7 @@ jobs:
|
|||||||
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.2.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -1353,14 +1378,14 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: pytest-*.txt
|
path: pytest-*.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
@ -1386,7 +1411,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'false'
|
if: needs.info.outputs.test_full_suite == 'false'
|
||||||
uses: codecov/codecov-action@v5.0.7
|
uses: codecov/codecov-action@v5.3.0
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -24,11 +24,11 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.27.5
|
uses: github/codeql-action/init@v3.28.4
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.27.5
|
uses: github/codeql-action/analyze@v3.28.4
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
# - No PRs marked as no-stale
|
# - No PRs marked as no-stale
|
||||||
# - No issues (-1)
|
# - No issues (-1)
|
||||||
- name: 60 days stale PRs policy
|
- name: 60 days stale PRs policy
|
||||||
uses: actions/stale@v9.0.0
|
uses: actions/stale@v9.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
@ -57,7 +57,7 @@ jobs:
|
|||||||
# - No issues marked as no-stale or help-wanted
|
# - No issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: 90 days stale issues
|
- name: 90 days stale issues
|
||||||
uses: actions/stale@v9.0.0
|
uses: actions/stale@v9.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
@ -87,7 +87,7 @@ jobs:
|
|||||||
# - No Issues marked as no-stale or help-wanted
|
# - No Issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: Needs more information stale issues policy
|
- name: Needs more information stale issues policy
|
||||||
uses: actions/stale@v9.0.0
|
uses: actions/stale@v9.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
only-labels: "needs-more-information"
|
only-labels: "needs-more-information"
|
||||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@ -10,7 +10,7 @@ on:
|
|||||||
- "**strings.json"
|
- "**strings.json"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEFAULT_PYTHON: "3.12"
|
DEFAULT_PYTHON: "3.13"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
upload:
|
upload:
|
||||||
|
74
.github/workflows/wheels.yml
vendored
74
.github/workflows/wheels.yml
vendored
@ -17,7 +17,7 @@ on:
|
|||||||
- "script/gen_requirements_all.py"
|
- "script/gen_requirements_all.py"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEFAULT_PYTHON: "3.12"
|
DEFAULT_PYTHON: "3.13"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref_name}}
|
group: ${{ github.workflow }}-${{ github.ref_name}}
|
||||||
@ -76,18 +76,37 @@ jobs:
|
|||||||
|
|
||||||
# Use C-Extension for SQLAlchemy
|
# Use C-Extension for SQLAlchemy
|
||||||
echo "REQUIRE_SQLALCHEMY_CEXT=1"
|
echo "REQUIRE_SQLALCHEMY_CEXT=1"
|
||||||
|
|
||||||
|
# Add additional pip wheel build constraints
|
||||||
|
echo "PIP_CONSTRAINT=build_constraints.txt"
|
||||||
) > .env_file
|
) > .env_file
|
||||||
|
|
||||||
|
- name: Write pip wheel build constraints
|
||||||
|
run: |
|
||||||
|
(
|
||||||
|
# ninja 1.11.1.2 + 1.11.1.3 seem to be broken on at least armhf
|
||||||
|
# this caused the numpy builds to fail
|
||||||
|
# https://github.com/scikit-build/ninja-python-distributions/issues/274
|
||||||
|
echo "ninja==1.11.1.1"
|
||||||
|
) > build_constraints.txt
|
||||||
|
|
||||||
- name: Upload env_file
|
- name: Upload env_file
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
path: ./.env_file
|
path: ./.env_file
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
|
- name: Upload build_constraints
|
||||||
|
uses: actions/upload-artifact@v4.6.0
|
||||||
|
with:
|
||||||
|
name: build_constraints
|
||||||
|
path: ./build_constraints.txt
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
- name: Upload requirements_diff
|
- name: Upload requirements_diff
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
path: ./requirements_diff.txt
|
path: ./requirements_diff.txt
|
||||||
@ -99,7 +118,7 @@ jobs:
|
|||||||
python -m script.gen_requirements_all ci
|
python -m script.gen_requirements_all ci
|
||||||
|
|
||||||
- name: Upload requirements_all_wheels
|
- name: Upload requirements_all_wheels
|
||||||
uses: actions/upload-artifact@v4.4.3
|
uses: actions/upload-artifact@v4.6.0
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
path: ./requirements_all_wheels_*.txt
|
path: ./requirements_all_wheels_*.txt
|
||||||
@ -123,6 +142,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
|
- name: Download build_constraints
|
||||||
|
uses: actions/download-artifact@v4.1.8
|
||||||
|
with:
|
||||||
|
name: build_constraints
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
@ -142,8 +166,8 @@ jobs:
|
|||||||
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;zlib-ng-dev"
|
||||||
skip-binary: aiohttp;multidict;yarl
|
skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements.txt"
|
requirements: "requirements.txt"
|
||||||
@ -167,6 +191,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
|
- name: Download build_constraints
|
||||||
|
uses: actions/download-artifact@v4.1.8
|
||||||
|
with:
|
||||||
|
name: build_constraints
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
@ -197,33 +226,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
|
|
||||||
if: matrix.abi == 'cp312'
|
|
||||||
run: |
|
|
||||||
# Some dependencies still require 'cython<3'
|
|
||||||
# and don't yet use isolated build environments.
|
|
||||||
# Build these first.
|
|
||||||
# pydantic: https://github.com/pydantic/pydantic/issues/7689
|
|
||||||
|
|
||||||
touch requirements_old-cython.txt
|
|
||||||
cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt
|
|
||||||
|
|
||||||
- name: Build wheels (old cython)
|
|
||||||
uses: home-assistant/wheels@2024.11.0
|
|
||||||
if: matrix.abi == 'cp312'
|
|
||||||
with:
|
|
||||||
abi: ${{ matrix.abi }}
|
|
||||||
tag: musllinux_1_2
|
|
||||||
arch: ${{ matrix.arch }}
|
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
|
||||||
env-file: true
|
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
|
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
|
||||||
requirements-diff: "requirements_diff.txt"
|
|
||||||
requirements: "requirements_old-cython.txt"
|
|
||||||
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.11.0
|
||||||
with:
|
with:
|
||||||
@ -232,7 +234,7 @@ jobs:
|
|||||||
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;zlib-ng-dev"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
@ -246,7 +248,7 @@ jobs:
|
|||||||
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;zlib-ng-dev"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
@ -260,7 +262,7 @@ jobs:
|
|||||||
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;zlib-ng-dev"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
|
@ -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.8.0
|
rev: v0.9.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
@ -12,7 +12,7 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
args:
|
args:
|
||||||
- --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn
|
- --ignore-words-list=aiport,astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn
|
||||||
- --skip="./.*,*.csv,*.json,*.ambr"
|
- --skip="./.*,*.csv,*.json,*.ambr"
|
||||||
- --quiet-level=2
|
- --quiet-level=2
|
||||||
exclude_types: [csv, json, html]
|
exclude_types: [csv, json, html]
|
||||||
@ -61,13 +61,14 @@ repos:
|
|||||||
name: mypy
|
name: mypy
|
||||||
entry: script/run-in-env.sh mypy
|
entry: script/run-in-env.sh mypy
|
||||||
language: script
|
language: script
|
||||||
types_or: [python, pyi]
|
|
||||||
require_serial: true
|
require_serial: true
|
||||||
|
types_or: [python, pyi]
|
||||||
files: ^(homeassistant|pylint)/.+\.(py|pyi)$
|
files: ^(homeassistant|pylint)/.+\.(py|pyi)$
|
||||||
- id: pylint
|
- id: pylint
|
||||||
name: pylint
|
name: pylint
|
||||||
entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y
|
entry: script/run-in-env.sh pylint --ignore-missing-annotations=y
|
||||||
language: script
|
language: script
|
||||||
|
require_serial: true
|
||||||
types_or: [python, pyi]
|
types_or: [python, pyi]
|
||||||
files: ^(homeassistant|tests)/.+\.(py|pyi)$
|
files: ^(homeassistant|tests)/.+\.(py|pyi)$
|
||||||
- id: gen_requirements_all
|
- id: gen_requirements_all
|
||||||
|
@ -41,6 +41,7 @@ homeassistant.util.unit_system
|
|||||||
# --- Add components below this line ---
|
# --- Add components below this line ---
|
||||||
homeassistant.components
|
homeassistant.components
|
||||||
homeassistant.components.abode.*
|
homeassistant.components.abode.*
|
||||||
|
homeassistant.components.acaia.*
|
||||||
homeassistant.components.accuweather.*
|
homeassistant.components.accuweather.*
|
||||||
homeassistant.components.acer_projector.*
|
homeassistant.components.acer_projector.*
|
||||||
homeassistant.components.acmeda.*
|
homeassistant.components.acmeda.*
|
||||||
@ -136,6 +137,7 @@ homeassistant.components.co2signal.*
|
|||||||
homeassistant.components.command_line.*
|
homeassistant.components.command_line.*
|
||||||
homeassistant.components.config.*
|
homeassistant.components.config.*
|
||||||
homeassistant.components.configurator.*
|
homeassistant.components.configurator.*
|
||||||
|
homeassistant.components.cookidoo.*
|
||||||
homeassistant.components.counter.*
|
homeassistant.components.counter.*
|
||||||
homeassistant.components.cover.*
|
homeassistant.components.cover.*
|
||||||
homeassistant.components.cpuspeed.*
|
homeassistant.components.cpuspeed.*
|
||||||
@ -168,6 +170,7 @@ homeassistant.components.easyenergy.*
|
|||||||
homeassistant.components.ecovacs.*
|
homeassistant.components.ecovacs.*
|
||||||
homeassistant.components.ecowitt.*
|
homeassistant.components.ecowitt.*
|
||||||
homeassistant.components.efergy.*
|
homeassistant.components.efergy.*
|
||||||
|
homeassistant.components.eheimdigital.*
|
||||||
homeassistant.components.electrasmart.*
|
homeassistant.components.electrasmart.*
|
||||||
homeassistant.components.electric_kiwi.*
|
homeassistant.components.electric_kiwi.*
|
||||||
homeassistant.components.elevenlabs.*
|
homeassistant.components.elevenlabs.*
|
||||||
@ -221,6 +224,7 @@ homeassistant.components.gpsd.*
|
|||||||
homeassistant.components.greeneye_monitor.*
|
homeassistant.components.greeneye_monitor.*
|
||||||
homeassistant.components.group.*
|
homeassistant.components.group.*
|
||||||
homeassistant.components.guardian.*
|
homeassistant.components.guardian.*
|
||||||
|
homeassistant.components.habitica.*
|
||||||
homeassistant.components.hardkernel.*
|
homeassistant.components.hardkernel.*
|
||||||
homeassistant.components.hardware.*
|
homeassistant.components.hardware.*
|
||||||
homeassistant.components.here_travel_time.*
|
homeassistant.components.here_travel_time.*
|
||||||
@ -233,6 +237,7 @@ homeassistant.components.homeassistant_green.*
|
|||||||
homeassistant.components.homeassistant_hardware.*
|
homeassistant.components.homeassistant_hardware.*
|
||||||
homeassistant.components.homeassistant_sky_connect.*
|
homeassistant.components.homeassistant_sky_connect.*
|
||||||
homeassistant.components.homeassistant_yellow.*
|
homeassistant.components.homeassistant_yellow.*
|
||||||
|
homeassistant.components.homee.*
|
||||||
homeassistant.components.homekit.*
|
homeassistant.components.homekit.*
|
||||||
homeassistant.components.homekit_controller
|
homeassistant.components.homekit_controller
|
||||||
homeassistant.components.homekit_controller.alarm_control_panel
|
homeassistant.components.homekit_controller.alarm_control_panel
|
||||||
@ -258,6 +263,7 @@ homeassistant.components.image_processing.*
|
|||||||
homeassistant.components.image_upload.*
|
homeassistant.components.image_upload.*
|
||||||
homeassistant.components.imap.*
|
homeassistant.components.imap.*
|
||||||
homeassistant.components.imgw_pib.*
|
homeassistant.components.imgw_pib.*
|
||||||
|
homeassistant.components.incomfort.*
|
||||||
homeassistant.components.input_button.*
|
homeassistant.components.input_button.*
|
||||||
homeassistant.components.input_select.*
|
homeassistant.components.input_select.*
|
||||||
homeassistant.components.input_text.*
|
homeassistant.components.input_text.*
|
||||||
@ -268,6 +274,7 @@ homeassistant.components.ios.*
|
|||||||
homeassistant.components.iotty.*
|
homeassistant.components.iotty.*
|
||||||
homeassistant.components.ipp.*
|
homeassistant.components.ipp.*
|
||||||
homeassistant.components.iqvia.*
|
homeassistant.components.iqvia.*
|
||||||
|
homeassistant.components.iron_os.*
|
||||||
homeassistant.components.islamic_prayer_times.*
|
homeassistant.components.islamic_prayer_times.*
|
||||||
homeassistant.components.isy994.*
|
homeassistant.components.isy994.*
|
||||||
homeassistant.components.jellyfin.*
|
homeassistant.components.jellyfin.*
|
||||||
@ -287,6 +294,7 @@ homeassistant.components.lcn.*
|
|||||||
homeassistant.components.ld2410_ble.*
|
homeassistant.components.ld2410_ble.*
|
||||||
homeassistant.components.led_ble.*
|
homeassistant.components.led_ble.*
|
||||||
homeassistant.components.lektrico.*
|
homeassistant.components.lektrico.*
|
||||||
|
homeassistant.components.letpot.*
|
||||||
homeassistant.components.lidarr.*
|
homeassistant.components.lidarr.*
|
||||||
homeassistant.components.lifx.*
|
homeassistant.components.lifx.*
|
||||||
homeassistant.components.light.*
|
homeassistant.components.light.*
|
||||||
@ -301,12 +309,15 @@ homeassistant.components.logbook.*
|
|||||||
homeassistant.components.logger.*
|
homeassistant.components.logger.*
|
||||||
homeassistant.components.london_underground.*
|
homeassistant.components.london_underground.*
|
||||||
homeassistant.components.lookin.*
|
homeassistant.components.lookin.*
|
||||||
|
homeassistant.components.lovelace.*
|
||||||
homeassistant.components.luftdaten.*
|
homeassistant.components.luftdaten.*
|
||||||
homeassistant.components.madvr.*
|
homeassistant.components.madvr.*
|
||||||
homeassistant.components.manual.*
|
homeassistant.components.manual.*
|
||||||
homeassistant.components.mastodon.*
|
homeassistant.components.mastodon.*
|
||||||
homeassistant.components.matrix.*
|
homeassistant.components.matrix.*
|
||||||
homeassistant.components.matter.*
|
homeassistant.components.matter.*
|
||||||
|
homeassistant.components.mcp_server.*
|
||||||
|
homeassistant.components.mealie.*
|
||||||
homeassistant.components.media_extractor.*
|
homeassistant.components.media_extractor.*
|
||||||
homeassistant.components.media_player.*
|
homeassistant.components.media_player.*
|
||||||
homeassistant.components.media_source.*
|
homeassistant.components.media_source.*
|
||||||
@ -357,13 +368,18 @@ homeassistant.components.openuv.*
|
|||||||
homeassistant.components.oralb.*
|
homeassistant.components.oralb.*
|
||||||
homeassistant.components.otbr.*
|
homeassistant.components.otbr.*
|
||||||
homeassistant.components.overkiz.*
|
homeassistant.components.overkiz.*
|
||||||
|
homeassistant.components.overseerr.*
|
||||||
homeassistant.components.p1_monitor.*
|
homeassistant.components.p1_monitor.*
|
||||||
|
homeassistant.components.pandora.*
|
||||||
homeassistant.components.panel_custom.*
|
homeassistant.components.panel_custom.*
|
||||||
|
homeassistant.components.peblar.*
|
||||||
homeassistant.components.peco.*
|
homeassistant.components.peco.*
|
||||||
homeassistant.components.persistent_notification.*
|
homeassistant.components.persistent_notification.*
|
||||||
|
homeassistant.components.person.*
|
||||||
homeassistant.components.pi_hole.*
|
homeassistant.components.pi_hole.*
|
||||||
homeassistant.components.ping.*
|
homeassistant.components.ping.*
|
||||||
homeassistant.components.plugwise.*
|
homeassistant.components.plugwise.*
|
||||||
|
homeassistant.components.powerfox.*
|
||||||
homeassistant.components.powerwall.*
|
homeassistant.components.powerwall.*
|
||||||
homeassistant.components.private_ble_device.*
|
homeassistant.components.private_ble_device.*
|
||||||
homeassistant.components.prometheus.*
|
homeassistant.components.prometheus.*
|
||||||
@ -373,6 +389,8 @@ homeassistant.components.pure_energie.*
|
|||||||
homeassistant.components.purpleair.*
|
homeassistant.components.purpleair.*
|
||||||
homeassistant.components.pushbullet.*
|
homeassistant.components.pushbullet.*
|
||||||
homeassistant.components.pvoutput.*
|
homeassistant.components.pvoutput.*
|
||||||
|
homeassistant.components.python_script.*
|
||||||
|
homeassistant.components.qbus.*
|
||||||
homeassistant.components.qnap_qsw.*
|
homeassistant.components.qnap_qsw.*
|
||||||
homeassistant.components.rabbitair.*
|
homeassistant.components.rabbitair.*
|
||||||
homeassistant.components.radarr.*
|
homeassistant.components.radarr.*
|
||||||
@ -400,11 +418,13 @@ homeassistant.components.romy.*
|
|||||||
homeassistant.components.rpi_power.*
|
homeassistant.components.rpi_power.*
|
||||||
homeassistant.components.rss_feed_template.*
|
homeassistant.components.rss_feed_template.*
|
||||||
homeassistant.components.rtsp_to_webrtc.*
|
homeassistant.components.rtsp_to_webrtc.*
|
||||||
|
homeassistant.components.russound_rio.*
|
||||||
homeassistant.components.ruuvi_gateway.*
|
homeassistant.components.ruuvi_gateway.*
|
||||||
homeassistant.components.ruuvitag_ble.*
|
homeassistant.components.ruuvitag_ble.*
|
||||||
homeassistant.components.samsungtv.*
|
homeassistant.components.samsungtv.*
|
||||||
homeassistant.components.scene.*
|
homeassistant.components.scene.*
|
||||||
homeassistant.components.schedule.*
|
homeassistant.components.schedule.*
|
||||||
|
homeassistant.components.schlage.*
|
||||||
homeassistant.components.scrape.*
|
homeassistant.components.scrape.*
|
||||||
homeassistant.components.script.*
|
homeassistant.components.script.*
|
||||||
homeassistant.components.search.*
|
homeassistant.components.search.*
|
||||||
@ -437,7 +457,6 @@ homeassistant.components.ssdp.*
|
|||||||
homeassistant.components.starlink.*
|
homeassistant.components.starlink.*
|
||||||
homeassistant.components.statistics.*
|
homeassistant.components.statistics.*
|
||||||
homeassistant.components.steamist.*
|
homeassistant.components.steamist.*
|
||||||
homeassistant.components.stookalert.*
|
|
||||||
homeassistant.components.stookwijzer.*
|
homeassistant.components.stookwijzer.*
|
||||||
homeassistant.components.stream.*
|
homeassistant.components.stream.*
|
||||||
homeassistant.components.streamlabswater.*
|
homeassistant.components.streamlabswater.*
|
||||||
|
3
.vscode/settings.default.json
vendored
3
.vscode/settings.default.json
vendored
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
// Please keep this file in sync with settings in home-assistant/.devcontainer/devcontainer.json
|
// Please keep this file (mostly!) in sync with settings in home-assistant/.devcontainer/devcontainer.json
|
||||||
// Added --no-cov to work around TypeError: message must be set
|
// Added --no-cov to work around TypeError: message must be set
|
||||||
// https://github.com/microsoft/vscode-python/issues/14067
|
// https://github.com/microsoft/vscode-python/issues/14067
|
||||||
"python.testing.pytestArgs": ["--no-cov"],
|
"python.testing.pytestArgs": ["--no-cov"],
|
||||||
@ -12,6 +12,7 @@
|
|||||||
"fileMatch": [
|
"fileMatch": [
|
||||||
"homeassistant/components/*/manifest.json"
|
"homeassistant/components/*/manifest.json"
|
||||||
],
|
],
|
||||||
|
// This value differs between working with devcontainer and locally, therefor this value should NOT be in sync!
|
||||||
"url": "./script/json_schemas/manifest_schema.json"
|
"url": "./script/json_schemas/manifest_schema.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
28
.vscode/tasks.json
vendored
28
.vscode/tasks.json
vendored
@ -16,7 +16,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Pytest",
|
"label": "Pytest",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "python3 -m pytest --timeout=10 tests",
|
"command": "${command:python.interpreterPath} -m pytest --timeout=10 tests",
|
||||||
"dependsOn": ["Install all Test Requirements"],
|
"dependsOn": ["Install all Test Requirements"],
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
@ -31,7 +31,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Pytest (changed tests only)",
|
"label": "Pytest (changed tests only)",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "python3 -m pytest --timeout=10 --picked",
|
"command": "${command:python.interpreterPath} -m pytest --timeout=10 --picked",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
@ -56,6 +56,20 @@
|
|||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Pre-commit",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pre-commit run --show-diff-on-failure",
|
||||||
|
"group": {
|
||||||
|
"kind": "test",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "new"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Pylint",
|
"label": "Pylint",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
@ -75,7 +89,7 @@
|
|||||||
"label": "Code Coverage",
|
"label": "Code Coverage",
|
||||||
"detail": "Generate code coverage report for a given integration.",
|
"detail": "Generate code coverage report for a given integration.",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "python3 -m pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0 --numprocesses=auto",
|
"command": "${command:python.interpreterPath} -m pytest ./tests/components/${input:integrationName}/ --cov=homeassistant.components.${input:integrationName} --cov-report term-missing --durations-min=1 --durations=0 --numprocesses=auto",
|
||||||
"dependsOn": ["Compile English translations"],
|
"dependsOn": ["Compile English translations"],
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
@ -91,7 +105,7 @@
|
|||||||
"label": "Update syrupy snapshots",
|
"label": "Update syrupy snapshots",
|
||||||
"detail": "Update syrupy snapshots for a given integration.",
|
"detail": "Update syrupy snapshots for a given integration.",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "python3 -m pytest ./tests/components/${input:integrationName} --snapshot-update",
|
"command": "${command:python.interpreterPath} -m pytest ./tests/components/${input:integrationName} --snapshot-update",
|
||||||
"dependsOn": ["Compile English translations"],
|
"dependsOn": ["Compile English translations"],
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
@ -149,7 +163,7 @@
|
|||||||
"label": "Compile English translations",
|
"label": "Compile English translations",
|
||||||
"detail": "In order to test changes to translation files, the translation strings must be compiled into Home Assistant's translation directories.",
|
"detail": "In order to test changes to translation files, the translation strings must be compiled into Home Assistant's translation directories.",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "python3 -m script.translations develop --all",
|
"command": "${command:python.interpreterPath} -m script.translations develop --all",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
@ -159,7 +173,7 @@
|
|||||||
"label": "Run scaffold",
|
"label": "Run scaffold",
|
||||||
"detail": "Add new functionality to a integration using a scaffold.",
|
"detail": "Add new functionality to a integration using a scaffold.",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "python3 -m script.scaffold ${input:scaffoldName} --integration ${input:integrationName}",
|
"command": "${command:python.interpreterPath} -m script.scaffold ${input:scaffoldName} --integration ${input:integrationName}",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
@ -169,7 +183,7 @@
|
|||||||
"label": "Create new integration",
|
"label": "Create new integration",
|
||||||
"detail": "Use the scaffold to create a new integration.",
|
"detail": "Use the scaffold to create a new integration.",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "python3 -m script.scaffold integration",
|
"command": "${command:python.interpreterPath} -m script.scaffold integration",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
|
89
CODEOWNERS
generated
89
CODEOWNERS
generated
@ -284,6 +284,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/control4/ @lawtancool
|
/tests/components/control4/ @lawtancool
|
||||||
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam
|
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam
|
||||||
/tests/components/conversation/ @home-assistant/core @synesthesiam
|
/tests/components/conversation/ @home-assistant/core @synesthesiam
|
||||||
|
/homeassistant/components/cookidoo/ @miaucl
|
||||||
|
/tests/components/cookidoo/ @miaucl
|
||||||
/homeassistant/components/coolmaster/ @OnFreund
|
/homeassistant/components/coolmaster/ @OnFreund
|
||||||
/tests/components/coolmaster/ @OnFreund
|
/tests/components/coolmaster/ @OnFreund
|
||||||
/homeassistant/components/counter/ @fabaff
|
/homeassistant/components/counter/ @fabaff
|
||||||
@ -385,6 +387,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/efergy/ @tkdrob
|
/homeassistant/components/efergy/ @tkdrob
|
||||||
/tests/components/efergy/ @tkdrob
|
/tests/components/efergy/ @tkdrob
|
||||||
/homeassistant/components/egardia/ @jeroenterheerdt
|
/homeassistant/components/egardia/ @jeroenterheerdt
|
||||||
|
/homeassistant/components/eheimdigital/ @autinerd
|
||||||
|
/tests/components/eheimdigital/ @autinerd
|
||||||
/homeassistant/components/electrasmart/ @jafar-atili
|
/homeassistant/components/electrasmart/ @jafar-atili
|
||||||
/tests/components/electrasmart/ @jafar-atili
|
/tests/components/electrasmart/ @jafar-atili
|
||||||
/homeassistant/components/electric_kiwi/ @mikey0000
|
/homeassistant/components/electric_kiwi/ @mikey0000
|
||||||
@ -574,8 +578,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/google_tasks/ @allenporter
|
/tests/components/google_tasks/ @allenporter
|
||||||
/homeassistant/components/google_travel_time/ @eifinger
|
/homeassistant/components/google_travel_time/ @eifinger
|
||||||
/tests/components/google_travel_time/ @eifinger
|
/tests/components/google_travel_time/ @eifinger
|
||||||
/homeassistant/components/govee_ble/ @bdraco @PierreAronnax
|
/homeassistant/components/govee_ble/ @bdraco
|
||||||
/tests/components/govee_ble/ @bdraco @PierreAronnax
|
/tests/components/govee_ble/ @bdraco
|
||||||
/homeassistant/components/govee_light_local/ @Galorhallen
|
/homeassistant/components/govee_light_local/ @Galorhallen
|
||||||
/tests/components/govee_light_local/ @Galorhallen
|
/tests/components/govee_light_local/ @Galorhallen
|
||||||
/homeassistant/components/gpsd/ @fabaff @jrieger
|
/homeassistant/components/gpsd/ @fabaff @jrieger
|
||||||
@ -633,6 +637,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/homeassistant_sky_connect/ @home-assistant/core
|
/tests/components/homeassistant_sky_connect/ @home-assistant/core
|
||||||
/homeassistant/components/homeassistant_yellow/ @home-assistant/core
|
/homeassistant/components/homeassistant_yellow/ @home-assistant/core
|
||||||
/tests/components/homeassistant_yellow/ @home-assistant/core
|
/tests/components/homeassistant_yellow/ @home-assistant/core
|
||||||
|
/homeassistant/components/homee/ @Taraman17
|
||||||
|
/tests/components/homee/ @Taraman17
|
||||||
/homeassistant/components/homekit/ @bdraco
|
/homeassistant/components/homekit/ @bdraco
|
||||||
/tests/components/homekit/ @bdraco
|
/tests/components/homekit/ @bdraco
|
||||||
/homeassistant/components/homekit_controller/ @Jc2k @bdraco
|
/homeassistant/components/homekit_controller/ @Jc2k @bdraco
|
||||||
@ -676,12 +682,12 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/iammeter/ @lewei50
|
/homeassistant/components/iammeter/ @lewei50
|
||||||
/homeassistant/components/iaqualink/ @flz
|
/homeassistant/components/iaqualink/ @flz
|
||||||
/tests/components/iaqualink/ @flz
|
/tests/components/iaqualink/ @flz
|
||||||
/homeassistant/components/ibeacon/ @bdraco
|
|
||||||
/tests/components/ibeacon/ @bdraco
|
|
||||||
/homeassistant/components/icloud/ @Quentame @nzapponi
|
/homeassistant/components/icloud/ @Quentame @nzapponi
|
||||||
/tests/components/icloud/ @Quentame @nzapponi
|
/tests/components/icloud/ @Quentame @nzapponi
|
||||||
/homeassistant/components/idasen_desk/ @abmantis
|
/homeassistant/components/idasen_desk/ @abmantis
|
||||||
/tests/components/idasen_desk/ @abmantis
|
/tests/components/idasen_desk/ @abmantis
|
||||||
|
/homeassistant/components/igloohome/ @keithle888
|
||||||
|
/tests/components/igloohome/ @keithle888
|
||||||
/homeassistant/components/ign_sismologia/ @exxamalte
|
/homeassistant/components/ign_sismologia/ @exxamalte
|
||||||
/tests/components/ign_sismologia/ @exxamalte
|
/tests/components/ign_sismologia/ @exxamalte
|
||||||
/homeassistant/components/image/ @home-assistant/core
|
/homeassistant/components/image/ @home-assistant/core
|
||||||
@ -727,8 +733,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ios/ @robbiet480
|
/tests/components/ios/ @robbiet480
|
||||||
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
||||||
/tests/components/iotawatt/ @gtdiehl @jyavenard
|
/tests/components/iotawatt/ @gtdiehl @jyavenard
|
||||||
/homeassistant/components/iotty/ @pburgio @shapournemati-iotty
|
/homeassistant/components/iotty/ @shapournemati-iotty
|
||||||
/tests/components/iotty/ @pburgio @shapournemati-iotty
|
/tests/components/iotty/ @shapournemati-iotty
|
||||||
/homeassistant/components/iperf3/ @rohankapoorcom
|
/homeassistant/components/iperf3/ @rohankapoorcom
|
||||||
/homeassistant/components/ipma/ @dgomes
|
/homeassistant/components/ipma/ @dgomes
|
||||||
/tests/components/ipma/ @dgomes
|
/tests/components/ipma/ @dgomes
|
||||||
@ -753,6 +759,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ista_ecotrend/ @tr4nt0r
|
/tests/components/ista_ecotrend/ @tr4nt0r
|
||||||
/homeassistant/components/isy994/ @bdraco @shbatm
|
/homeassistant/components/isy994/ @bdraco @shbatm
|
||||||
/tests/components/isy994/ @bdraco @shbatm
|
/tests/components/isy994/ @bdraco @shbatm
|
||||||
|
/homeassistant/components/ituran/ @shmuelzon
|
||||||
|
/tests/components/ituran/ @shmuelzon
|
||||||
/homeassistant/components/izone/ @Swamp-Ig
|
/homeassistant/components/izone/ @Swamp-Ig
|
||||||
/tests/components/izone/ @Swamp-Ig
|
/tests/components/izone/ @Swamp-Ig
|
||||||
/homeassistant/components/jellyfin/ @j-stienstra @ctalkington
|
/homeassistant/components/jellyfin/ @j-stienstra @ctalkington
|
||||||
@ -821,6 +829,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/led_ble/ @bdraco
|
/tests/components/led_ble/ @bdraco
|
||||||
/homeassistant/components/lektrico/ @lektrico
|
/homeassistant/components/lektrico/ @lektrico
|
||||||
/tests/components/lektrico/ @lektrico
|
/tests/components/lektrico/ @lektrico
|
||||||
|
/homeassistant/components/letpot/ @jpelgrom
|
||||||
|
/tests/components/letpot/ @jpelgrom
|
||||||
/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
|
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
@ -881,6 +891,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/matrix/ @PaarthShah
|
/tests/components/matrix/ @PaarthShah
|
||||||
/homeassistant/components/matter/ @home-assistant/matter
|
/homeassistant/components/matter/ @home-assistant/matter
|
||||||
/tests/components/matter/ @home-assistant/matter
|
/tests/components/matter/ @home-assistant/matter
|
||||||
|
/homeassistant/components/mcp_server/ @allenporter
|
||||||
|
/tests/components/mcp_server/ @allenporter
|
||||||
/homeassistant/components/mealie/ @joostlek @andrew-codechimp
|
/homeassistant/components/mealie/ @joostlek @andrew-codechimp
|
||||||
/tests/components/mealie/ @joostlek @andrew-codechimp
|
/tests/components/mealie/ @joostlek @andrew-codechimp
|
||||||
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
||||||
@ -1004,11 +1016,12 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/nice_go/ @IceBotYT
|
/tests/components/nice_go/ @IceBotYT
|
||||||
/homeassistant/components/nightscout/ @marciogranzotto
|
/homeassistant/components/nightscout/ @marciogranzotto
|
||||||
/tests/components/nightscout/ @marciogranzotto
|
/tests/components/nightscout/ @marciogranzotto
|
||||||
|
/homeassistant/components/niko_home_control/ @VandeurenGlenn
|
||||||
|
/tests/components/niko_home_control/ @VandeurenGlenn
|
||||||
/homeassistant/components/nilu/ @hfurubotten
|
/homeassistant/components/nilu/ @hfurubotten
|
||||||
/homeassistant/components/nina/ @DeerMaximum
|
/homeassistant/components/nina/ @DeerMaximum
|
||||||
/tests/components/nina/ @DeerMaximum
|
/tests/components/nina/ @DeerMaximum
|
||||||
/homeassistant/components/nissan_leaf/ @filcole
|
/homeassistant/components/nissan_leaf/ @filcole
|
||||||
/homeassistant/components/nmbs/ @thibmaek
|
|
||||||
/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
|
||||||
@ -1045,6 +1058,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/octoprint/ @rfleming71
|
/homeassistant/components/octoprint/ @rfleming71
|
||||||
/tests/components/octoprint/ @rfleming71
|
/tests/components/octoprint/ @rfleming71
|
||||||
/homeassistant/components/ohmconnect/ @robbiet480
|
/homeassistant/components/ohmconnect/ @robbiet480
|
||||||
|
/homeassistant/components/ohme/ @dan-r
|
||||||
|
/tests/components/ohme/ @dan-r
|
||||||
/homeassistant/components/ollama/ @synesthesiam
|
/homeassistant/components/ollama/ @synesthesiam
|
||||||
/tests/components/ollama/ @synesthesiam
|
/tests/components/ollama/ @synesthesiam
|
||||||
/homeassistant/components/ombi/ @larssont
|
/homeassistant/components/ombi/ @larssont
|
||||||
@ -1056,10 +1071,10 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ondilo_ico/ @JeromeHXP
|
/tests/components/ondilo_ico/ @JeromeHXP
|
||||||
/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 @eclair4151
|
||||||
/tests/components/onkyo/ @arturpragacz
|
/tests/components/onkyo/ @arturpragacz @eclair4151
|
||||||
/homeassistant/components/onvif/ @hunterjm
|
/homeassistant/components/onvif/ @hunterjm @jterrace
|
||||||
/tests/components/onvif/ @hunterjm
|
/tests/components/onvif/ @hunterjm @jterrace
|
||||||
/homeassistant/components/open_meteo/ @frenck
|
/homeassistant/components/open_meteo/ @frenck
|
||||||
/tests/components/open_meteo/ @frenck
|
/tests/components/open_meteo/ @frenck
|
||||||
/homeassistant/components/openai_conversation/ @balloob
|
/homeassistant/components/openai_conversation/ @balloob
|
||||||
@ -1093,8 +1108,10 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/otbr/ @home-assistant/core
|
/tests/components/otbr/ @home-assistant/core
|
||||||
/homeassistant/components/ourgroceries/ @OnFreund
|
/homeassistant/components/ourgroceries/ @OnFreund
|
||||||
/tests/components/ourgroceries/ @OnFreund
|
/tests/components/ourgroceries/ @OnFreund
|
||||||
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
|
/homeassistant/components/overkiz/ @imicknl
|
||||||
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
|
/tests/components/overkiz/ @imicknl
|
||||||
|
/homeassistant/components/overseerr/ @joostlek
|
||||||
|
/tests/components/overseerr/ @joostlek
|
||||||
/homeassistant/components/ovo_energy/ @timmo001
|
/homeassistant/components/ovo_energy/ @timmo001
|
||||||
/tests/components/ovo_energy/ @timmo001
|
/tests/components/ovo_energy/ @timmo001
|
||||||
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
||||||
@ -1103,6 +1120,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/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/peblar/ @frenck
|
||||||
|
/tests/components/peblar/ @frenck
|
||||||
/homeassistant/components/peco/ @IceBotYT
|
/homeassistant/components/peco/ @IceBotYT
|
||||||
/tests/components/peco/ @IceBotYT
|
/tests/components/peco/ @IceBotYT
|
||||||
/homeassistant/components/pegel_online/ @mib1185
|
/homeassistant/components/pegel_online/ @mib1185
|
||||||
@ -1123,14 +1142,16 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/plaato/ @JohNan
|
/tests/components/plaato/ @JohNan
|
||||||
/homeassistant/components/plex/ @jjlawren
|
/homeassistant/components/plex/ @jjlawren
|
||||||
/tests/components/plex/ @jjlawren
|
/tests/components/plex/ @jjlawren
|
||||||
/homeassistant/components/plugwise/ @CoMPaTech @bouwew @frenck
|
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
|
||||||
/tests/components/plugwise/ @CoMPaTech @bouwew @frenck
|
/tests/components/plugwise/ @CoMPaTech @bouwew
|
||||||
/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa
|
/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa
|
||||||
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
|
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
|
||||||
/homeassistant/components/point/ @fredrike
|
/homeassistant/components/point/ @fredrike
|
||||||
/tests/components/point/ @fredrike
|
/tests/components/point/ @fredrike
|
||||||
/homeassistant/components/poolsense/ @haemishkyd
|
/homeassistant/components/poolsense/ @haemishkyd
|
||||||
/tests/components/poolsense/ @haemishkyd
|
/tests/components/poolsense/ @haemishkyd
|
||||||
|
/homeassistant/components/powerfox/ @klaasnicolaas
|
||||||
|
/tests/components/powerfox/ @klaasnicolaas
|
||||||
/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson
|
/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson
|
||||||
/tests/components/powerwall/ @bdraco @jrester @daniel-simpson
|
/tests/components/powerwall/ @bdraco @jrester @daniel-simpson
|
||||||
/homeassistant/components/private_ble_device/ @Jc2k
|
/homeassistant/components/private_ble_device/ @Jc2k
|
||||||
@ -1168,6 +1189,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/pyload/ @tr4nt0r
|
/tests/components/pyload/ @tr4nt0r
|
||||||
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
|
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||||
/tests/components/qbittorrent/ @geoffreylagaisse @finder39
|
/tests/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||||
|
/homeassistant/components/qbus/ @Qbus-iot @thomasddn
|
||||||
|
/tests/components/qbus/ @Qbus-iot @thomasddn
|
||||||
/homeassistant/components/qingping/ @bdraco
|
/homeassistant/components/qingping/ @bdraco
|
||||||
/tests/components/qingping/ @bdraco
|
/tests/components/qingping/ @bdraco
|
||||||
/homeassistant/components/qld_bushfire/ @exxamalte
|
/homeassistant/components/qld_bushfire/ @exxamalte
|
||||||
@ -1244,8 +1267,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
|
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
|
||||||
/homeassistant/components/rmvtransport/ @cgtobi
|
/homeassistant/components/rmvtransport/ @cgtobi
|
||||||
/tests/components/rmvtransport/ @cgtobi
|
/tests/components/rmvtransport/ @cgtobi
|
||||||
/homeassistant/components/roborock/ @Lash-L
|
/homeassistant/components/roborock/ @Lash-L @allenporter
|
||||||
/tests/components/roborock/ @Lash-L
|
/tests/components/roborock/ @Lash-L @allenporter
|
||||||
/homeassistant/components/roku/ @ctalkington
|
/homeassistant/components/roku/ @ctalkington
|
||||||
/tests/components/roku/ @ctalkington
|
/tests/components/roku/ @ctalkington
|
||||||
/homeassistant/components/romy/ @xeniter
|
/homeassistant/components/romy/ @xeniter
|
||||||
@ -1264,6 +1287,7 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
||||||
/homeassistant/components/russound_rio/ @noahhusby
|
/homeassistant/components/russound_rio/ @noahhusby
|
||||||
/tests/components/russound_rio/ @noahhusby
|
/tests/components/russound_rio/ @noahhusby
|
||||||
|
/homeassistant/components/russound_rnet/ @noahhusby
|
||||||
/homeassistant/components/ruuvi_gateway/ @akx
|
/homeassistant/components/ruuvi_gateway/ @akx
|
||||||
/tests/components/ruuvi_gateway/ @akx
|
/tests/components/ruuvi_gateway/ @akx
|
||||||
/homeassistant/components/ruuvitag_ble/ @akx
|
/homeassistant/components/ruuvitag_ble/ @akx
|
||||||
@ -1353,10 +1377,12 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/sleepiq/ @mfugate1 @kbickar
|
/homeassistant/components/sleepiq/ @mfugate1 @kbickar
|
||||||
/tests/components/sleepiq/ @mfugate1 @kbickar
|
/tests/components/sleepiq/ @mfugate1 @kbickar
|
||||||
/homeassistant/components/slide/ @ualex73
|
/homeassistant/components/slide/ @ualex73
|
||||||
|
/homeassistant/components/slide_local/ @dontinelli
|
||||||
|
/tests/components/slide_local/ @dontinelli
|
||||||
/homeassistant/components/slimproto/ @marcelveldt
|
/homeassistant/components/slimproto/ @marcelveldt
|
||||||
/tests/components/slimproto/ @marcelveldt
|
/tests/components/slimproto/ @marcelveldt
|
||||||
/homeassistant/components/sma/ @kellerza @rklomp
|
/homeassistant/components/sma/ @kellerza @rklomp @erwindouna
|
||||||
/tests/components/sma/ @kellerza @rklomp
|
/tests/components/sma/ @kellerza @rklomp @erwindouna
|
||||||
/homeassistant/components/smappee/ @bsmappee
|
/homeassistant/components/smappee/ @bsmappee
|
||||||
/tests/components/smappee/ @bsmappee
|
/tests/components/smappee/ @bsmappee
|
||||||
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
||||||
@ -1411,15 +1437,13 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/starline/ @anonym-tsk
|
/tests/components/starline/ @anonym-tsk
|
||||||
/homeassistant/components/starlink/ @boswelja
|
/homeassistant/components/starlink/ @boswelja
|
||||||
/tests/components/starlink/ @boswelja
|
/tests/components/starlink/ @boswelja
|
||||||
/homeassistant/components/statistics/ @ThomDietrich
|
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
|
||||||
/tests/components/statistics/ @ThomDietrich
|
/tests/components/statistics/ @ThomDietrich @gjohansson-ST
|
||||||
/homeassistant/components/steam_online/ @tkdrob
|
/homeassistant/components/steam_online/ @tkdrob
|
||||||
/tests/components/steam_online/ @tkdrob
|
/tests/components/steam_online/ @tkdrob
|
||||||
/homeassistant/components/steamist/ @bdraco
|
/homeassistant/components/steamist/ @bdraco
|
||||||
/tests/components/steamist/ @bdraco
|
/tests/components/steamist/ @bdraco
|
||||||
/homeassistant/components/stiebel_eltron/ @fucm
|
/homeassistant/components/stiebel_eltron/ @fucm
|
||||||
/homeassistant/components/stookalert/ @fwestenberg @frenck
|
|
||||||
/tests/components/stookalert/ @fwestenberg @frenck
|
|
||||||
/homeassistant/components/stookwijzer/ @fwestenberg
|
/homeassistant/components/stookwijzer/ @fwestenberg
|
||||||
/tests/components/stookwijzer/ @fwestenberg
|
/tests/components/stookwijzer/ @fwestenberg
|
||||||
/homeassistant/components/stream/ @hunterjm @uvjustin @allenporter
|
/homeassistant/components/stream/ @hunterjm @uvjustin @allenporter
|
||||||
@ -1464,8 +1488,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/system_bridge/ @timmo001
|
/tests/components/system_bridge/ @timmo001
|
||||||
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
||||||
/tests/components/systemmonitor/ @gjohansson-ST
|
/tests/components/systemmonitor/ @gjohansson-ST
|
||||||
/homeassistant/components/tado/ @chiefdragon @erwindouna
|
/homeassistant/components/tado/ @erwindouna
|
||||||
/tests/components/tado/ @chiefdragon @erwindouna
|
/tests/components/tado/ @erwindouna
|
||||||
/homeassistant/components/tag/ @balloob @dmulcahey
|
/homeassistant/components/tag/ @balloob @dmulcahey
|
||||||
/tests/components/tag/ @balloob @dmulcahey
|
/tests/components/tag/ @balloob @dmulcahey
|
||||||
/homeassistant/components/tailscale/ @frenck
|
/homeassistant/components/tailscale/ @frenck
|
||||||
@ -1559,8 +1583,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/triggercmd/ @rvmey
|
/tests/components/triggercmd/ @rvmey
|
||||||
/homeassistant/components/tts/ @home-assistant/core
|
/homeassistant/components/tts/ @home-assistant/core
|
||||||
/tests/components/tts/ @home-assistant/core
|
/tests/components/tts/ @home-assistant/core
|
||||||
/homeassistant/components/tuya/ @Tuya @zlinoliver @frenck
|
/homeassistant/components/tuya/ @Tuya @zlinoliver
|
||||||
/tests/components/tuya/ @Tuya @zlinoliver @frenck
|
/tests/components/tuya/ @Tuya @zlinoliver
|
||||||
/homeassistant/components/twentemilieu/ @frenck
|
/homeassistant/components/twentemilieu/ @frenck
|
||||||
/tests/components/twentemilieu/ @frenck
|
/tests/components/twentemilieu/ @frenck
|
||||||
/homeassistant/components/twinkly/ @dr1rrb @Robbie1221 @Olen
|
/homeassistant/components/twinkly/ @dr1rrb @Robbie1221 @Olen
|
||||||
@ -1604,15 +1628,15 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/valve/ @home-assistant/core
|
/tests/components/valve/ @home-assistant/core
|
||||||
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
||||||
/tests/components/velbus/ @Cereal2nd @brefra
|
/tests/components/velbus/ @Cereal2nd @brefra
|
||||||
/homeassistant/components/velux/ @Julius2342 @DeerMaximum
|
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio
|
||||||
/tests/components/velux/ @Julius2342 @DeerMaximum
|
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio
|
||||||
/homeassistant/components/venstar/ @garbled1 @jhollowe
|
/homeassistant/components/venstar/ @garbled1 @jhollowe
|
||||||
/tests/components/venstar/ @garbled1 @jhollowe
|
/tests/components/venstar/ @garbled1 @jhollowe
|
||||||
/homeassistant/components/versasense/ @imstevenxyz
|
/homeassistant/components/versasense/ @imstevenxyz
|
||||||
/homeassistant/components/version/ @ludeeus
|
/homeassistant/components/version/ @ludeeus
|
||||||
/tests/components/version/ @ludeeus
|
/tests/components/version/ @ludeeus
|
||||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
|
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak
|
||||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
|
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak
|
||||||
/homeassistant/components/vicare/ @CFenner
|
/homeassistant/components/vicare/ @CFenner
|
||||||
/tests/components/vicare/ @CFenner
|
/tests/components/vicare/ @CFenner
|
||||||
/homeassistant/components/vilfo/ @ManneW
|
/homeassistant/components/vilfo/ @ManneW
|
||||||
@ -1642,6 +1666,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/waqi/ @joostlek
|
/tests/components/waqi/ @joostlek
|
||||||
/homeassistant/components/water_heater/ @home-assistant/core
|
/homeassistant/components/water_heater/ @home-assistant/core
|
||||||
/tests/components/water_heater/ @home-assistant/core
|
/tests/components/water_heater/ @home-assistant/core
|
||||||
|
/homeassistant/components/watergate/ @adam-the-hero
|
||||||
|
/tests/components/watergate/ @adam-the-hero
|
||||||
/homeassistant/components/watson_tts/ @rutkai
|
/homeassistant/components/watson_tts/ @rutkai
|
||||||
/homeassistant/components/watttime/ @bachya
|
/homeassistant/components/watttime/ @bachya
|
||||||
/tests/components/watttime/ @bachya
|
/tests/components/watttime/ @bachya
|
||||||
@ -1726,6 +1752,7 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/youless/ @gjong
|
/tests/components/youless/ @gjong
|
||||||
/homeassistant/components/youtube/ @joostlek
|
/homeassistant/components/youtube/ @joostlek
|
||||||
/tests/components/youtube/ @joostlek
|
/tests/components/youtube/ @joostlek
|
||||||
|
/homeassistant/components/zabbix/ @kruton
|
||||||
/homeassistant/components/zamg/ @killer0071234
|
/homeassistant/components/zamg/ @killer0071234
|
||||||
/tests/components/zamg/ @killer0071234
|
/tests/components/zamg/ @killer0071234
|
||||||
/homeassistant/components/zengge/ @emontnemery
|
/homeassistant/components/zengge/ @emontnemery
|
||||||
|
4
Dockerfile
generated
4
Dockerfile
generated
@ -13,7 +13,7 @@ ENV \
|
|||||||
ARG QEMU_CPU
|
ARG QEMU_CPU
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
RUN pip3 install uv==0.5.4
|
RUN pip3 install uv==0.5.21
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
@ -55,7 +55,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.8/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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM mcr.microsoft.com/devcontainers/python:1-3.12
|
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
|
10
build.yaml
10
build.yaml
@ -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.12.0
|
||||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.11.0
|
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.12.0
|
||||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.11.0
|
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.12.0
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.11.0
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.12.0
|
||||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.11.0
|
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.12.0
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
|
@ -115,7 +115,7 @@ class AuthManagerFlowManager(
|
|||||||
*,
|
*,
|
||||||
context: AuthFlowContext | None = None,
|
context: AuthFlowContext | None = None,
|
||||||
data: dict[str, Any] | None = None,
|
data: dict[str, Any] | None = None,
|
||||||
) -> LoginFlow:
|
) -> LoginFlow[Any]:
|
||||||
"""Create a login flow."""
|
"""Create a login flow."""
|
||||||
auth_provider = self.auth_manager.get_auth_provider(*handler_key)
|
auth_provider = self.auth_manager.get_auth_provider(*handler_key)
|
||||||
if not auth_provider:
|
if not auth_provider:
|
||||||
|
@ -308,7 +308,7 @@ class AuthStore:
|
|||||||
credentials.data = data
|
credentials.data = data
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
|
||||||
async def async_load(self) -> None: # noqa: C901
|
async def async_load(self) -> None:
|
||||||
"""Load the users."""
|
"""Load the users."""
|
||||||
if self._loaded:
|
if self._loaded:
|
||||||
raise RuntimeError("Auth storage is already loaded")
|
raise RuntimeError("Auth storage is already loaded")
|
||||||
|
@ -71,7 +71,7 @@ class MultiFactorAuthModule:
|
|||||||
"""Return a voluptuous schema to define mfa auth module's input."""
|
"""Return a voluptuous schema to define mfa auth module's input."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
async def async_setup_flow(self, user_id: str) -> SetupFlow[Any]:
|
||||||
"""Return a data entry flow handler for setup module.
|
"""Return a data entry flow handler for setup module.
|
||||||
|
|
||||||
Mfa module should extend SetupFlow
|
Mfa module should extend SetupFlow
|
||||||
@ -95,11 +95,16 @@ class MultiFactorAuthModule:
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class SetupFlow(data_entry_flow.FlowHandler):
|
class SetupFlow[_MultiFactorAuthModuleT: MultiFactorAuthModule = MultiFactorAuthModule](
|
||||||
|
data_entry_flow.FlowHandler
|
||||||
|
):
|
||||||
"""Handler for the setup flow."""
|
"""Handler for the setup flow."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, auth_module: MultiFactorAuthModule, setup_schema: vol.Schema, user_id: str
|
self,
|
||||||
|
auth_module: _MultiFactorAuthModuleT,
|
||||||
|
setup_schema: vol.Schema,
|
||||||
|
user_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the setup flow."""
|
"""Initialize the setup flow."""
|
||||||
self._auth_module = auth_module
|
self._auth_module = auth_module
|
||||||
|
@ -162,7 +162,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
|||||||
|
|
||||||
return sorted(unordered_services)
|
return sorted(unordered_services)
|
||||||
|
|
||||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
async def async_setup_flow(self, user_id: str) -> NotifySetupFlow:
|
||||||
"""Return a data entry flow handler for setup module.
|
"""Return a data entry flow handler for setup module.
|
||||||
|
|
||||||
Mfa module should extend SetupFlow
|
Mfa module should extend SetupFlow
|
||||||
@ -268,7 +268,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
|||||||
await self.hass.services.async_call("notify", notify_service, data)
|
await self.hass.services.async_call("notify", notify_service, data)
|
||||||
|
|
||||||
|
|
||||||
class NotifySetupFlow(SetupFlow):
|
class NotifySetupFlow(SetupFlow[NotifyAuthModule]):
|
||||||
"""Handler for the setup flow."""
|
"""Handler for the setup flow."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -280,8 +280,6 @@ class NotifySetupFlow(SetupFlow):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the setup flow."""
|
"""Initialize the setup flow."""
|
||||||
super().__init__(auth_module, setup_schema, user_id)
|
super().__init__(auth_module, setup_schema, user_id)
|
||||||
# to fix typing complaint
|
|
||||||
self._auth_module: NotifyAuthModule = auth_module
|
|
||||||
self._available_notify_services = available_notify_services
|
self._available_notify_services = available_notify_services
|
||||||
self._secret: str | None = None
|
self._secret: str | None = None
|
||||||
self._count: int | None = None
|
self._count: int | None = None
|
||||||
|
@ -114,7 +114,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||||||
self._users[user_id] = ota_secret # type: ignore[index]
|
self._users[user_id] = ota_secret # type: ignore[index]
|
||||||
return ota_secret
|
return ota_secret
|
||||||
|
|
||||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
async def async_setup_flow(self, user_id: str) -> TotpSetupFlow:
|
||||||
"""Return a data entry flow handler for setup module.
|
"""Return a data entry flow handler for setup module.
|
||||||
|
|
||||||
Mfa module should extend SetupFlow
|
Mfa module should extend SetupFlow
|
||||||
@ -174,10 +174,9 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||||||
return bool(pyotp.TOTP(ota_secret).verify(code, valid_window=1))
|
return bool(pyotp.TOTP(ota_secret).verify(code, valid_window=1))
|
||||||
|
|
||||||
|
|
||||||
class TotpSetupFlow(SetupFlow):
|
class TotpSetupFlow(SetupFlow[TotpAuthModule]):
|
||||||
"""Handler for the setup flow."""
|
"""Handler for the setup flow."""
|
||||||
|
|
||||||
_auth_module: TotpAuthModule
|
|
||||||
_ota_secret: str
|
_ota_secret: str
|
||||||
_url: str
|
_url: str
|
||||||
_image: str
|
_image: str
|
||||||
|
@ -11,7 +11,7 @@ import uuid
|
|||||||
import attr
|
import attr
|
||||||
from attr import Attribute
|
from attr import Attribute
|
||||||
from attr.setters import validate
|
from attr.setters import validate
|
||||||
from propcache import cached_property
|
from propcache.api import cached_property
|
||||||
|
|
||||||
from homeassistant.const import __version__
|
from homeassistant.const import __version__
|
||||||
from homeassistant.data_entry_flow import FlowContext, FlowResult
|
from homeassistant.data_entry_flow import FlowContext, FlowResult
|
||||||
|
@ -17,12 +17,12 @@ POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA})
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"POLICY_SCHEMA",
|
"POLICY_SCHEMA",
|
||||||
"merge_policies",
|
|
||||||
"PermissionLookup",
|
|
||||||
"PolicyType",
|
|
||||||
"AbstractPermissions",
|
"AbstractPermissions",
|
||||||
"PolicyPermissions",
|
|
||||||
"OwnerPermissions",
|
"OwnerPermissions",
|
||||||
|
"PermissionLookup",
|
||||||
|
"PolicyPermissions",
|
||||||
|
"PolicyType",
|
||||||
|
"merge_policies",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ class AuthProvider:
|
|||||||
|
|
||||||
# Implement by extending class
|
# Implement by extending class
|
||||||
|
|
||||||
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow[Any]:
|
||||||
"""Return the data flow for logging in with auth provider.
|
"""Return the data flow for logging in with auth provider.
|
||||||
|
|
||||||
Auth provider should extend LoginFlow and return an instance.
|
Auth provider should extend LoginFlow and return an instance.
|
||||||
@ -192,12 +192,14 @@ async def load_auth_provider_module(
|
|||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
class LoginFlow(FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]]):
|
class LoginFlow[_AuthProviderT: AuthProvider = AuthProvider](
|
||||||
|
FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]],
|
||||||
|
):
|
||||||
"""Handler for the login flow."""
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
_flow_result = AuthFlowResult
|
_flow_result = AuthFlowResult
|
||||||
|
|
||||||
def __init__(self, auth_provider: AuthProvider) -> None:
|
def __init__(self, auth_provider: _AuthProviderT) -> None:
|
||||||
"""Initialize the login flow."""
|
"""Initialize the login flow."""
|
||||||
self._auth_provider = auth_provider
|
self._auth_provider = auth_provider
|
||||||
self._auth_module_id: str | None = None
|
self._auth_module_id: str | None = None
|
||||||
|
@ -6,7 +6,7 @@ import asyncio
|
|||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any, cast
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -59,7 +59,9 @@ class CommandLineAuthProvider(AuthProvider):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._user_meta: dict[str, dict[str, Any]] = {}
|
self._user_meta: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
async def async_login_flow(
|
||||||
|
self, context: AuthFlowContext | None
|
||||||
|
) -> CommandLineLoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
return CommandLineLoginFlow(self)
|
return CommandLineLoginFlow(self)
|
||||||
|
|
||||||
@ -133,7 +135,7 @@ class CommandLineAuthProvider(AuthProvider):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CommandLineLoginFlow(LoginFlow):
|
class CommandLineLoginFlow(LoginFlow[CommandLineAuthProvider]):
|
||||||
"""Handler for the login flow."""
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
@ -145,9 +147,9 @@ class CommandLineLoginFlow(LoginFlow):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
user_input["username"] = user_input["username"].strip()
|
user_input["username"] = user_input["username"].strip()
|
||||||
try:
|
try:
|
||||||
await cast(
|
await self._auth_provider.async_validate_login(
|
||||||
CommandLineAuthProvider, self._auth_provider
|
user_input["username"], user_input["password"]
|
||||||
).async_validate_login(user_input["username"], user_input["password"])
|
)
|
||||||
except InvalidAuthError:
|
except InvalidAuthError:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
|
|
||||||
|
@ -305,7 +305,7 @@ class HassAuthProvider(AuthProvider):
|
|||||||
await data.async_load()
|
await data.async_load()
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
async def async_login_flow(self, context: AuthFlowContext | None) -> HassLoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
return HassLoginFlow(self)
|
return HassLoginFlow(self)
|
||||||
|
|
||||||
@ -400,7 +400,7 @@ class HassAuthProvider(AuthProvider):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class HassLoginFlow(LoginFlow):
|
class HassLoginFlow(LoginFlow[HassAuthProvider]):
|
||||||
"""Handler for the login flow."""
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
@ -411,7 +411,7 @@ class HassLoginFlow(LoginFlow):
|
|||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
await cast(HassAuthProvider, self._auth_provider).async_validate_login(
|
await self._auth_provider.async_validate_login(
|
||||||
user_input["username"], user_input["password"]
|
user_input["username"], user_input["password"]
|
||||||
)
|
)
|
||||||
except InvalidAuth:
|
except InvalidAuth:
|
||||||
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
import hmac
|
import hmac
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -36,7 +35,9 @@ class InvalidAuthError(HomeAssistantError):
|
|||||||
class ExampleAuthProvider(AuthProvider):
|
class ExampleAuthProvider(AuthProvider):
|
||||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||||
|
|
||||||
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
async def async_login_flow(
|
||||||
|
self, context: AuthFlowContext | None
|
||||||
|
) -> ExampleLoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
return ExampleLoginFlow(self)
|
return ExampleLoginFlow(self)
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ class ExampleAuthProvider(AuthProvider):
|
|||||||
return UserMeta(name=name, is_active=True)
|
return UserMeta(name=name, is_active=True)
|
||||||
|
|
||||||
|
|
||||||
class ExampleLoginFlow(LoginFlow):
|
class ExampleLoginFlow(LoginFlow[ExampleAuthProvider]):
|
||||||
"""Handler for the login flow."""
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
@ -104,7 +105,7 @@ class ExampleLoginFlow(LoginFlow):
|
|||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
cast(ExampleAuthProvider, self._auth_provider).async_validate_login(
|
self._auth_provider.async_validate_login(
|
||||||
user_input["username"], user_input["password"]
|
user_input["username"], user_input["password"]
|
||||||
)
|
)
|
||||||
except InvalidAuthError:
|
except InvalidAuthError:
|
||||||
|
@ -104,7 +104,9 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
|||||||
"""Trusted Networks auth provider does not support MFA."""
|
"""Trusted Networks auth provider does not support MFA."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
async def async_login_flow(
|
||||||
|
self, context: AuthFlowContext | None
|
||||||
|
) -> TrustedNetworksLoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
assert context is not None
|
assert context is not None
|
||||||
ip_addr = cast(IPAddress, context.get("ip_address"))
|
ip_addr = cast(IPAddress, context.get("ip_address"))
|
||||||
@ -214,7 +216,7 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
|||||||
self.async_validate_access(ip_address(remote_ip))
|
self.async_validate_access(ip_address(remote_ip))
|
||||||
|
|
||||||
|
|
||||||
class TrustedNetworksLoginFlow(LoginFlow):
|
class TrustedNetworksLoginFlow(LoginFlow[TrustedNetworksAuthProvider]):
|
||||||
"""Handler for the login flow."""
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -235,9 +237,7 @@ class TrustedNetworksLoginFlow(LoginFlow):
|
|||||||
) -> AuthFlowResult:
|
) -> AuthFlowResult:
|
||||||
"""Handle the step of the form."""
|
"""Handle the step of the form."""
|
||||||
try:
|
try:
|
||||||
cast(
|
self._auth_provider.async_validate_access(self._ip_address)
|
||||||
TrustedNetworksAuthProvider, self._auth_provider
|
|
||||||
).async_validate_access(self._ip_address)
|
|
||||||
|
|
||||||
except InvalidAuthError:
|
except InvalidAuthError:
|
||||||
return self.async_abort(reason="not_allowed")
|
return self.async_abort(reason="not_allowed")
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
"""Home Assistant module to handle restoring backups."""
|
"""Home Assistant module to handle restoring backups."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -14,7 +18,12 @@ import securetar
|
|||||||
from .const import __version__ as HA_VERSION
|
from .const import __version__ as HA_VERSION
|
||||||
|
|
||||||
RESTORE_BACKUP_FILE = ".HA_RESTORE"
|
RESTORE_BACKUP_FILE = ".HA_RESTORE"
|
||||||
KEEP_PATHS = ("backups",)
|
KEEP_BACKUPS = ("backups",)
|
||||||
|
KEEP_DATABASE = (
|
||||||
|
"home-assistant_v2.db",
|
||||||
|
"home-assistant_v2.db-wal",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -24,6 +33,21 @@ class RestoreBackupFileContent:
|
|||||||
"""Definition for restore backup file content."""
|
"""Definition for restore backup file content."""
|
||||||
|
|
||||||
backup_file_path: Path
|
backup_file_path: Path
|
||||||
|
password: str | None
|
||||||
|
remove_after_restore: bool
|
||||||
|
restore_database: bool
|
||||||
|
restore_homeassistant: bool
|
||||||
|
|
||||||
|
|
||||||
|
def password_to_key(password: str) -> bytes:
|
||||||
|
"""Generate a AES Key from password.
|
||||||
|
|
||||||
|
Matches the implementation in supervisor.backups.utils.password_to_key.
|
||||||
|
"""
|
||||||
|
key: bytes = password.encode()
|
||||||
|
for _ in range(100):
|
||||||
|
key = hashlib.sha256(key).digest()
|
||||||
|
return key[:16]
|
||||||
|
|
||||||
|
|
||||||
def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent | None:
|
def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent | None:
|
||||||
@ -32,20 +56,27 @@ def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent |
|
|||||||
try:
|
try:
|
||||||
instruction_content = json.loads(instruction_path.read_text(encoding="utf-8"))
|
instruction_content = json.loads(instruction_path.read_text(encoding="utf-8"))
|
||||||
return RestoreBackupFileContent(
|
return RestoreBackupFileContent(
|
||||||
backup_file_path=Path(instruction_content["path"])
|
backup_file_path=Path(instruction_content["path"]),
|
||||||
|
password=instruction_content["password"],
|
||||||
|
remove_after_restore=instruction_content["remove_after_restore"],
|
||||||
|
restore_database=instruction_content["restore_database"],
|
||||||
|
restore_homeassistant=instruction_content["restore_homeassistant"],
|
||||||
)
|
)
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
except (FileNotFoundError, KeyError, json.JSONDecodeError):
|
||||||
return None
|
return None
|
||||||
|
finally:
|
||||||
|
# Always remove the backup instruction file to prevent a boot loop
|
||||||
|
instruction_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def _clear_configuration_directory(config_dir: Path) -> None:
|
def _clear_configuration_directory(config_dir: Path, keep: Iterable[str]) -> None:
|
||||||
"""Delete all files and directories in the config directory except for the backups directory."""
|
"""Delete all files and directories in the config directory except entries in the keep list."""
|
||||||
keep_paths = [config_dir.joinpath(path) for path in KEEP_PATHS]
|
keep_paths = [config_dir.joinpath(path) for path in keep]
|
||||||
config_contents = sorted(
|
entries_to_remove = sorted(
|
||||||
[entry for entry in config_dir.iterdir() if entry not in keep_paths]
|
entry for entry in config_dir.iterdir() if entry not in keep_paths
|
||||||
)
|
)
|
||||||
|
|
||||||
for entry in config_contents:
|
for entry in entries_to_remove:
|
||||||
entrypath = config_dir.joinpath(entry)
|
entrypath = config_dir.joinpath(entry)
|
||||||
|
|
||||||
if entrypath.is_file():
|
if entrypath.is_file():
|
||||||
@ -54,12 +85,15 @@ def _clear_configuration_directory(config_dir: Path) -> None:
|
|||||||
shutil.rmtree(entrypath)
|
shutil.rmtree(entrypath)
|
||||||
|
|
||||||
|
|
||||||
def _extract_backup(config_dir: Path, backup_file_path: Path) -> None:
|
def _extract_backup(
|
||||||
|
config_dir: Path,
|
||||||
|
restore_content: RestoreBackupFileContent,
|
||||||
|
) -> None:
|
||||||
"""Extract the backup file to the config directory."""
|
"""Extract the backup file to the config directory."""
|
||||||
with (
|
with (
|
||||||
TemporaryDirectory() as tempdir,
|
TemporaryDirectory() as tempdir,
|
||||||
securetar.SecureTarFile(
|
securetar.SecureTarFile(
|
||||||
backup_file_path,
|
restore_content.backup_file_path,
|
||||||
gzip=False,
|
gzip=False,
|
||||||
mode="r",
|
mode="r",
|
||||||
) as ostf,
|
) as ostf,
|
||||||
@ -85,25 +119,44 @@ def _extract_backup(config_dir: Path, backup_file_path: Path) -> None:
|
|||||||
Path(
|
Path(
|
||||||
tempdir,
|
tempdir,
|
||||||
"extracted",
|
"extracted",
|
||||||
f"homeassistant.tar{'.gz' if backup_meta["compressed"] else ''}",
|
f"homeassistant.tar{'.gz' if backup_meta['compressed'] else ''}",
|
||||||
),
|
),
|
||||||
gzip=backup_meta["compressed"],
|
gzip=backup_meta["compressed"],
|
||||||
|
key=password_to_key(restore_content.password)
|
||||||
|
if restore_content.password is not None
|
||||||
|
else None,
|
||||||
mode="r",
|
mode="r",
|
||||||
) as istf:
|
) 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(
|
istf.extractall(
|
||||||
path=config_dir,
|
path=Path(tempdir, "homeassistant"),
|
||||||
members=[
|
members=securetar.secure_path(istf),
|
||||||
member
|
|
||||||
for member in securetar.secure_path(istf)
|
|
||||||
if member.name != "data"
|
|
||||||
],
|
|
||||||
filter="fully_trusted",
|
filter="fully_trusted",
|
||||||
)
|
)
|
||||||
|
if restore_content.restore_homeassistant:
|
||||||
|
keep = list(KEEP_BACKUPS)
|
||||||
|
if not restore_content.restore_database:
|
||||||
|
keep.extend(KEEP_DATABASE)
|
||||||
|
_clear_configuration_directory(config_dir, keep)
|
||||||
|
shutil.copytree(
|
||||||
|
Path(tempdir, "homeassistant", "data"),
|
||||||
|
config_dir,
|
||||||
|
dirs_exist_ok=True,
|
||||||
|
ignore=shutil.ignore_patterns(*(keep)),
|
||||||
|
)
|
||||||
|
elif restore_content.restore_database:
|
||||||
|
for entry in KEEP_DATABASE:
|
||||||
|
entrypath = config_dir / entry
|
||||||
|
|
||||||
|
if entrypath.is_file():
|
||||||
|
entrypath.unlink()
|
||||||
|
elif entrypath.is_dir():
|
||||||
|
shutil.rmtree(entrypath)
|
||||||
|
|
||||||
|
for entry in KEEP_DATABASE:
|
||||||
|
shutil.copy(
|
||||||
|
Path(tempdir, "homeassistant", "data", entry),
|
||||||
|
config_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def restore_backup(config_dir_path: str) -> bool:
|
def restore_backup(config_dir_path: str) -> bool:
|
||||||
@ -119,8 +172,13 @@ def restore_backup(config_dir_path: str) -> bool:
|
|||||||
backup_file_path = restore_content.backup_file_path
|
backup_file_path = restore_content.backup_file_path
|
||||||
_LOGGER.info("Restoring %s", backup_file_path)
|
_LOGGER.info("Restoring %s", backup_file_path)
|
||||||
try:
|
try:
|
||||||
_extract_backup(config_dir, backup_file_path)
|
_extract_backup(
|
||||||
|
config_dir=config_dir,
|
||||||
|
restore_content=restore_content,
|
||||||
|
)
|
||||||
except FileNotFoundError as err:
|
except FileNotFoundError as err:
|
||||||
raise ValueError(f"Backup file {backup_file_path} does not exist") from err
|
raise ValueError(f"Backup file {backup_file_path} does not exist") from err
|
||||||
|
if restore_content.remove_after_restore:
|
||||||
|
backup_file_path.unlink(missing_ok=True)
|
||||||
_LOGGER.info("Restore complete, restarting")
|
_LOGGER.info("Restore complete, restarting")
|
||||||
return True
|
return True
|
||||||
|
@ -31,7 +31,7 @@ def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
|||||||
def _check_file_allowed(mapped_args: dict[str, Any]) -> bool:
|
def _check_file_allowed(mapped_args: dict[str, Any]) -> bool:
|
||||||
# If the file is in /proc we can ignore it.
|
# If the file is in /proc we can ignore it.
|
||||||
args = mapped_args["args"]
|
args = mapped_args["args"]
|
||||||
path = args[0] if type(args[0]) is str else str(args[0]) # noqa: E721
|
path = args[0] if type(args[0]) is str else str(args[0])
|
||||||
return path.startswith(ALLOWED_FILE_PREFIXES)
|
return path.startswith(ALLOWED_FILE_PREFIXES)
|
||||||
|
|
||||||
|
|
||||||
@ -50,6 +50,12 @@ def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _check_load_verify_locations_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
||||||
|
# If only cadata is passed, we can ignore it
|
||||||
|
kwargs = mapped_args.get("kwargs")
|
||||||
|
return bool(kwargs and len(kwargs) == 1 and "cadata" in kwargs)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
@dataclass(slots=True, frozen=True)
|
||||||
class BlockingCall:
|
class BlockingCall:
|
||||||
"""Class to hold information about a blocking call."""
|
"""Class to hold information about a blocking call."""
|
||||||
@ -158,7 +164,7 @@ _BLOCKING_CALLS: tuple[BlockingCall, ...] = (
|
|||||||
original_func=SSLContext.load_verify_locations,
|
original_func=SSLContext.load_verify_locations,
|
||||||
object=SSLContext,
|
object=SSLContext,
|
||||||
function="load_verify_locations",
|
function="load_verify_locations",
|
||||||
check_allowed=None,
|
check_allowed=_check_load_verify_locations_call_allowed,
|
||||||
strict=False,
|
strict=False,
|
||||||
strict_core=False,
|
strict_core=False,
|
||||||
skip_for_tests=True,
|
skip_for_tests=True,
|
||||||
|
@ -89,7 +89,7 @@ from .helpers import (
|
|||||||
)
|
)
|
||||||
from .helpers.dispatcher import async_dispatcher_send_internal
|
from .helpers.dispatcher import async_dispatcher_send_internal
|
||||||
from .helpers.storage import get_internal_store_manager
|
from .helpers.storage import get_internal_store_manager
|
||||||
from .helpers.system_info import async_get_system_info, is_official_image
|
from .helpers.system_info import async_get_system_info
|
||||||
from .helpers.typing import ConfigType
|
from .helpers.typing import ConfigType
|
||||||
from .setup import (
|
from .setup import (
|
||||||
# _setup_started is marked as protected to make it clear
|
# _setup_started is marked as protected to make it clear
|
||||||
@ -106,11 +106,17 @@ from .util.async_ import create_eager_task
|
|||||||
from .util.hass_dict import HassKey
|
from .util.hass_dict import HassKey
|
||||||
from .util.logging import async_activate_log_queue_handler
|
from .util.logging import async_activate_log_queue_handler
|
||||||
from .util.package import async_get_user_site, is_docker_env, is_virtual_env
|
from .util.package import async_get_user_site, is_docker_env, is_virtual_env
|
||||||
|
from .util.system_info import is_official_image
|
||||||
|
|
||||||
with contextlib.suppress(ImportError):
|
with contextlib.suppress(ImportError):
|
||||||
# Ensure anyio backend is imported to avoid it being imported in the event loop
|
# Ensure anyio backend is imported to avoid it being imported in the event loop
|
||||||
from anyio._backends import _asyncio # noqa: F401
|
from anyio._backends import _asyncio # noqa: F401
|
||||||
|
|
||||||
|
with contextlib.suppress(ImportError):
|
||||||
|
# httpx will import trio if it is installed which does
|
||||||
|
# blocking I/O in the event loop. We want to avoid that.
|
||||||
|
import trio # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .runner import RuntimeConfig
|
from .runner import RuntimeConfig
|
||||||
@ -252,6 +258,7 @@ PRELOAD_STORAGE = [
|
|||||||
"assist_pipeline.pipelines",
|
"assist_pipeline.pipelines",
|
||||||
"core.analytics",
|
"core.analytics",
|
||||||
"auth_module.totp",
|
"auth_module.totp",
|
||||||
|
"backup",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"domain": "microsoft",
|
"domain": "microsoft",
|
||||||
"name": "Microsoft",
|
"name": "Microsoft",
|
||||||
"integrations": [
|
"integrations": [
|
||||||
|
"azure_data_explorer",
|
||||||
"azure_devops",
|
"azure_devops",
|
||||||
"azure_event_hub",
|
"azure_event_hub",
|
||||||
"azure_service_bus",
|
"azure_service_bus",
|
||||||
|
5
homeassistant/brands/slide.json
Normal file
5
homeassistant/brands/slide.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "slide",
|
||||||
|
"name": "Slide",
|
||||||
|
"integrations": ["slide", "slide_local"]
|
||||||
|
}
|
@ -9,18 +9,16 @@ from jaraco.abode.devices.light import Light
|
|||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
ATTR_COLOR_TEMP,
|
ATTR_COLOR_TEMP_KELVIN,
|
||||||
ATTR_HS_COLOR,
|
ATTR_HS_COLOR,
|
||||||
|
DEFAULT_MAX_KELVIN,
|
||||||
|
DEFAULT_MIN_KELVIN,
|
||||||
ColorMode,
|
ColorMode,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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.color import (
|
|
||||||
color_temperature_kelvin_to_mired,
|
|
||||||
color_temperature_mired_to_kelvin,
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import AbodeSystem
|
from . import AbodeSystem
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@ -44,13 +42,13 @@ class AbodeLight(AbodeDevice, LightEntity):
|
|||||||
|
|
||||||
_device: Light
|
_device: Light
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
|
_attr_max_color_temp_kelvin = DEFAULT_MAX_KELVIN
|
||||||
|
_attr_min_color_temp_kelvin = DEFAULT_MIN_KELVIN
|
||||||
|
|
||||||
def turn_on(self, **kwargs: Any) -> None:
|
def turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn on the light."""
|
"""Turn on the light."""
|
||||||
if ATTR_COLOR_TEMP in kwargs and self._device.is_color_capable:
|
if ATTR_COLOR_TEMP_KELVIN in kwargs and self._device.is_color_capable:
|
||||||
self._device.set_color_temp(
|
self._device.set_color_temp(kwargs[ATTR_COLOR_TEMP_KELVIN])
|
||||||
int(color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]))
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if ATTR_HS_COLOR in kwargs and self._device.is_color_capable:
|
if ATTR_HS_COLOR in kwargs and self._device.is_color_capable:
|
||||||
@ -85,10 +83,10 @@ class AbodeLight(AbodeDevice, LightEntity):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def color_temp(self) -> int | None:
|
def color_temp_kelvin(self) -> int | None:
|
||||||
"""Return the color temp of the light."""
|
"""Return the color temp of the light."""
|
||||||
if self._device.has_color:
|
if self._device.has_color:
|
||||||
return color_temperature_kelvin_to_mired(self._device.color_temp)
|
return int(self._device.color_temp)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -34,17 +34,17 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"capture_image": {
|
"capture_image": {
|
||||||
"name": "Capture image",
|
"name": "Capture image",
|
||||||
"description": "Request a new image capture from a camera device.",
|
"description": "Requests a new image capture from a camera device.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"entity_id": {
|
"entity_id": {
|
||||||
"name": "Entity",
|
"name": "Entity",
|
||||||
"description": "Entity id of the camera to request an image."
|
"description": "Entity ID of the camera to request an image from."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"change_setting": {
|
"change_setting": {
|
||||||
"name": "Change setting",
|
"name": "Change setting",
|
||||||
"description": "Change an Abode system setting.",
|
"description": "Changes an Abode system setting.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"setting": {
|
"setting": {
|
||||||
"name": "Setting",
|
"name": "Setting",
|
||||||
@ -58,11 +58,11 @@
|
|||||||
},
|
},
|
||||||
"trigger_automation": {
|
"trigger_automation": {
|
||||||
"name": "Trigger automation",
|
"name": "Trigger automation",
|
||||||
"description": "Trigger an Abode automation.",
|
"description": "Triggers an Abode automation.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"entity_id": {
|
"entity_id": {
|
||||||
"name": "Entity",
|
"name": "Entity",
|
||||||
"description": "Entity id of the automation to trigger."
|
"description": "Entity ID of the automation to trigger."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from .coordinator import AcaiaConfigEntry
|
from .coordinator import AcaiaConfigEntry
|
||||||
from .entity import AcaiaEntity
|
from .entity import AcaiaEntity
|
||||||
|
|
||||||
|
# Coordinator is used to centralize the data updates
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True, frozen=True)
|
@dataclass(kw_only=True, frozen=True)
|
||||||
class AcaiaBinarySensorEntityDescription(BinarySensorEntityDescription):
|
class AcaiaBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||||
|
@ -42,7 +42,7 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
mac = format_mac(user_input[CONF_ADDRESS])
|
mac = user_input[CONF_ADDRESS]
|
||||||
try:
|
try:
|
||||||
is_new_style_scale = await is_new_scale(mac)
|
is_new_style_scale = await is_new_scale(mac)
|
||||||
except AcaiaDeviceNotFound:
|
except AcaiaDeviceNotFound:
|
||||||
@ -53,12 +53,12 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
except AcaiaUnknownDevice:
|
except AcaiaUnknownDevice:
|
||||||
return self.async_abort(reason="unsupported_device")
|
return self.async_abort(reason="unsupported_device")
|
||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(mac)
|
await self.async_set_unique_id(format_mac(mac))
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
if not errors:
|
if not errors:
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=self._discovered_devices[user_input[CONF_ADDRESS]],
|
title=self._discovered_devices[mac],
|
||||||
data={
|
data={
|
||||||
CONF_ADDRESS: mac,
|
CONF_ADDRESS: mac,
|
||||||
CONF_IS_NEW_STYLE_SCALE: is_new_style_scale,
|
CONF_IS_NEW_STYLE_SCALE: is_new_style_scale,
|
||||||
@ -99,10 +99,10 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle a discovered Bluetooth device."""
|
"""Handle a discovered Bluetooth device."""
|
||||||
|
|
||||||
self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address)
|
self._discovered[CONF_ADDRESS] = discovery_info.address
|
||||||
self._discovered[CONF_NAME] = discovery_info.name
|
self._discovered[CONF_NAME] = discovery_info.name
|
||||||
|
|
||||||
await self.async_set_unique_id(mac)
|
await self.async_set_unique_id(format_mac(discovery_info.address))
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import (
|
||||||
|
CONNECTION_BLUETOOTH,
|
||||||
|
DeviceInfo,
|
||||||
|
format_mac,
|
||||||
|
)
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
@ -25,13 +29,15 @@ class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]):
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
self._scale = coordinator.scale
|
self._scale = coordinator.scale
|
||||||
self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}"
|
formatted_mac = format_mac(self._scale.mac)
|
||||||
|
self._attr_unique_id = f"{formatted_mac}_{entity_description.key}"
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, self._scale.mac)},
|
identifiers={(DOMAIN, formatted_mac)},
|
||||||
manufacturer="Acaia",
|
manufacturer="Acaia",
|
||||||
model=self._scale.model,
|
model=self._scale.model,
|
||||||
suggested_area="Kitchen",
|
suggested_area="Kitchen",
|
||||||
|
connections={(CONNECTION_BLUETOOTH, self._scale.mac)},
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aioacaia"],
|
"loggers": ["aioacaia"],
|
||||||
"requirements": ["aioacaia==0.1.9"]
|
"quality_scale": "platinum",
|
||||||
|
"requirements": ["aioacaia==0.1.14"]
|
||||||
}
|
}
|
||||||
|
106
homeassistant/components/acaia/quality_scale.yaml
Normal file
106
homeassistant/components/acaia/quality_scale.yaml
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No custom actions are defined.
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No custom actions are defined.
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No explicit event subscriptions.
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure: done
|
||||||
|
test-before-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Device is expected to be offline most of the time, but needs to connect quickly once available.
|
||||||
|
unique-config-entry: done
|
||||||
|
# Silver
|
||||||
|
action-exceptions:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No custom actions are defined.
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters: done
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable: done
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable:
|
||||||
|
status: done
|
||||||
|
comment: |
|
||||||
|
Handled by coordinator.
|
||||||
|
parallel-updates: done
|
||||||
|
reauthentication-flow:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No authentication required.
|
||||||
|
test-coverage: done
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: done
|
||||||
|
discovery-update-info:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No IP discovery.
|
||||||
|
discovery:
|
||||||
|
status: done
|
||||||
|
comment: |
|
||||||
|
Bluetooth discovery.
|
||||||
|
docs-data-update: done
|
||||||
|
docs-examples: done
|
||||||
|
docs-known-limitations: done
|
||||||
|
docs-supported-devices: done
|
||||||
|
docs-supported-functions: done
|
||||||
|
docs-troubleshooting: done
|
||||||
|
docs-use-cases: done
|
||||||
|
dynamic-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Device type integration.
|
||||||
|
entity-category: done
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No noisy/non-essential entities.
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No custom exceptions.
|
||||||
|
icon-translations: done
|
||||||
|
reconfiguration-flow:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Only parameter that could be changed (MAC = unique_id) would force a new config entry.
|
||||||
|
repair-issues:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No repairs/issues.
|
||||||
|
stale-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Device type integration.
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Bluetooth connection.
|
||||||
|
strict-typing: done
|
@ -21,6 +21,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from .coordinator import AcaiaConfigEntry
|
from .coordinator import AcaiaConfigEntry
|
||||||
from .entity import AcaiaEntity
|
from .entity import AcaiaEntity
|
||||||
|
|
||||||
|
# Coordinator is used to centralize the data updates
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True, frozen=True)
|
@dataclass(kw_only=True, frozen=True)
|
||||||
class AcaiaSensorEntityDescription(SensorEntityDescription):
|
class AcaiaSensorEntityDescription(SensorEntityDescription):
|
||||||
|
@ -70,7 +70,7 @@ class PulseHub:
|
|||||||
|
|
||||||
async def async_notify_update(self, update_type: aiopulse.UpdateType) -> None:
|
async def async_notify_update(self, update_type: aiopulse.UpdateType) -> None:
|
||||||
"""Evaluate entities when hub reports that update has occurred."""
|
"""Evaluate entities when hub reports that update has occurred."""
|
||||||
LOGGER.debug("Hub {update_type.name} updated")
|
LOGGER.debug("Hub %s updated", update_type.name)
|
||||||
|
|
||||||
if update_type == aiopulse.UpdateType.rollers:
|
if update_type == aiopulse.UpdateType.rollers:
|
||||||
await update_devices(self.hass, self.config_entry, self.api.rollers)
|
await update_devices(self.hass, self.config_entry, self.api.rollers)
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import telnetlib # pylint: disable=deprecated-module
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
|
import telnetlib # pylint: disable=deprecated-module
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
|
@ -75,7 +75,6 @@ class AdaxDevice(ClimateEntity):
|
|||||||
)
|
)
|
||||||
_attr_target_temperature_step = PRECISION_WHOLE
|
_attr_target_temperature_step = PRECISION_WHOLE
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
_enable_turn_on_off_backwards_compatibility = False
|
|
||||||
|
|
||||||
def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None:
|
def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None:
|
||||||
"""Initialize the heater."""
|
"""Initialize the heater."""
|
||||||
|
@ -34,9 +34,12 @@ from .const import (
|
|||||||
SERVICE_REMOVE_URL,
|
SERVICE_REMOVE_URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
SERVICE_URL_SCHEMA = vol.Schema({vol.Required(CONF_URL): cv.url})
|
SERVICE_URL_SCHEMA = vol.Schema({vol.Required(CONF_URL): vol.Any(cv.url, cv.path)})
|
||||||
SERVICE_ADD_URL_SCHEMA = vol.Schema(
|
SERVICE_ADD_URL_SCHEMA = vol.Schema(
|
||||||
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_URL): cv.url}
|
{
|
||||||
|
vol.Required(CONF_NAME): cv.string,
|
||||||
|
vol.Required(CONF_URL): vol.Any(cv.url, cv.path),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
SERVICE_REFRESH_SCHEMA = vol.Schema(
|
SERVICE_REFRESH_SCHEMA = vol.Schema(
|
||||||
{vol.Optional(CONF_FORCE, default=False): cv.boolean}
|
{vol.Optional(CONF_FORCE, default=False): cv.boolean}
|
||||||
|
@ -37,7 +37,7 @@ STATE_KEY_POSITION = "position"
|
|||||||
|
|
||||||
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_ADS_VAR): cv.string,
|
vol.Required(CONF_ADS_VAR): cv.string,
|
||||||
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
|
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
|
||||||
vol.Optional(CONF_ADS_VAR_SET_POS): cv.string,
|
vol.Optional(CONF_ADS_VAR_SET_POS): cv.string,
|
||||||
vol.Optional(CONF_ADS_VAR_CLOSE): cv.string,
|
vol.Optional(CONF_ADS_VAR_CLOSE): cv.string,
|
||||||
|
@ -66,7 +66,7 @@ class AdvantageAirZoneMotion(AdvantageAirZoneEntity, BinarySensorEntity):
|
|||||||
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
||||||
"""Initialize an Advantage Air Zone Motion sensor."""
|
"""Initialize an Advantage Air Zone Motion sensor."""
|
||||||
super().__init__(instance, ac_key, zone_key)
|
super().__init__(instance, ac_key, zone_key)
|
||||||
self._attr_name = f'{self._zone["name"]} motion'
|
self._attr_name = f"{self._zone['name']} motion"
|
||||||
self._attr_unique_id += "-motion"
|
self._attr_unique_id += "-motion"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -84,7 +84,7 @@ class AdvantageAirZoneMyZone(AdvantageAirZoneEntity, BinarySensorEntity):
|
|||||||
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
||||||
"""Initialize an Advantage Air Zone MyZone sensor."""
|
"""Initialize an Advantage Air Zone MyZone sensor."""
|
||||||
super().__init__(instance, ac_key, zone_key)
|
super().__init__(instance, ac_key, zone_key)
|
||||||
self._attr_name = f'{self._zone["name"]} myZone'
|
self._attr_name = f"{self._zone['name']} myZone"
|
||||||
self._attr_unique_id += "-myzone"
|
self._attr_unique_id += "-myzone"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -102,7 +102,6 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
|||||||
_attr_max_temp = 32
|
_attr_max_temp = 32
|
||||||
_attr_min_temp = 16
|
_attr_min_temp = 16
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
_enable_turn_on_off_backwards_compatibility = False
|
|
||||||
_support_preset = ClimateEntityFeature(0)
|
_support_preset = ClimateEntityFeature(0)
|
||||||
|
|
||||||
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
|
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
|
||||||
@ -261,7 +260,6 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity):
|
|||||||
_attr_target_temperature_step = PRECISION_WHOLE
|
_attr_target_temperature_step = PRECISION_WHOLE
|
||||||
_attr_max_temp = 32
|
_attr_max_temp = 32
|
||||||
_attr_min_temp = 16
|
_attr_min_temp = 16
|
||||||
_enable_turn_on_off_backwards_compatibility = False
|
|
||||||
|
|
||||||
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
||||||
"""Initialize an AdvantageAir Zone control."""
|
"""Initialize an AdvantageAir Zone control."""
|
||||||
|
@ -103,7 +103,7 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, SensorEntity):
|
|||||||
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
||||||
"""Initialize an Advantage Air Zone Vent Sensor."""
|
"""Initialize an Advantage Air Zone Vent Sensor."""
|
||||||
super().__init__(instance, ac_key, zone_key=zone_key)
|
super().__init__(instance, ac_key, zone_key=zone_key)
|
||||||
self._attr_name = f'{self._zone["name"]} vent'
|
self._attr_name = f"{self._zone['name']} vent"
|
||||||
self._attr_unique_id += "-vent"
|
self._attr_unique_id += "-vent"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -131,7 +131,7 @@ class AdvantageAirZoneSignal(AdvantageAirZoneEntity, SensorEntity):
|
|||||||
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
||||||
"""Initialize an Advantage Air Zone wireless signal sensor."""
|
"""Initialize an Advantage Air Zone wireless signal sensor."""
|
||||||
super().__init__(instance, ac_key, zone_key)
|
super().__init__(instance, ac_key, zone_key)
|
||||||
self._attr_name = f'{self._zone["name"]} signal'
|
self._attr_name = f"{self._zone['name']} signal"
|
||||||
self._attr_unique_id += "-signal"
|
self._attr_unique_id += "-signal"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -165,7 +165,7 @@ class AdvantageAirZoneTemp(AdvantageAirZoneEntity, SensorEntity):
|
|||||||
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
||||||
"""Initialize an Advantage Air Zone Temp Sensor."""
|
"""Initialize an Advantage Air Zone Temp Sensor."""
|
||||||
super().__init__(instance, ac_key, zone_key)
|
super().__init__(instance, ac_key, zone_key)
|
||||||
self._attr_name = f'{self._zone["name"]} temperature'
|
self._attr_name = f"{self._zone['name']} temperature"
|
||||||
self._attr_unique_id += "-temp"
|
self._attr_unique_id += "-temp"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""The AEMET OpenData component."""
|
"""The AEMET OpenData component."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import shutil
|
||||||
|
|
||||||
from aemet_opendata.exceptions import AemetError, TownNotFound
|
from aemet_opendata.exceptions import AemetError, TownNotFound
|
||||||
from aemet_opendata.interface import AEMET, ConnectionOptions, UpdateFeature
|
from aemet_opendata.interface import AEMET, ConnectionOptions, UpdateFeature
|
||||||
@ -10,8 +11,9 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CON
|
|||||||
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
|
||||||
|
from homeassistant.helpers.storage import STORAGE_DIR
|
||||||
|
|
||||||
from .const import CONF_STATION_UPDATES, PLATFORMS
|
from .const import CONF_RADAR_UPDATES, CONF_STATION_UPDATES, DOMAIN, PLATFORMS
|
||||||
from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator
|
from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -24,11 +26,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
|
|||||||
latitude = entry.data[CONF_LATITUDE]
|
latitude = entry.data[CONF_LATITUDE]
|
||||||
longitude = entry.data[CONF_LONGITUDE]
|
longitude = entry.data[CONF_LONGITUDE]
|
||||||
update_features: int = UpdateFeature.FORECAST
|
update_features: int = UpdateFeature.FORECAST
|
||||||
|
if entry.options.get(CONF_RADAR_UPDATES, False):
|
||||||
|
update_features |= UpdateFeature.RADAR
|
||||||
if entry.options.get(CONF_STATION_UPDATES, True):
|
if entry.options.get(CONF_STATION_UPDATES, True):
|
||||||
update_features |= UpdateFeature.STATION
|
update_features |= UpdateFeature.STATION
|
||||||
|
|
||||||
options = ConnectionOptions(api_key, update_features)
|
options = ConnectionOptions(api_key, update_features)
|
||||||
aemet = AEMET(aiohttp_client.async_get_clientsession(hass), options)
|
aemet = AEMET(aiohttp_client.async_get_clientsession(hass), options)
|
||||||
|
aemet.set_api_data_dir(hass.config.path(STORAGE_DIR, f"{DOMAIN}-{entry.unique_id}"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await aemet.select_coordinates(latitude, longitude)
|
await aemet.select_coordinates(latitude, longitude)
|
||||||
except TownNotFound as err:
|
except TownNotFound as err:
|
||||||
@ -57,3 +63,11 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Remove a config entry."""
|
||||||
|
await hass.async_add_executor_job(
|
||||||
|
shutil.rmtree,
|
||||||
|
hass.config.path(STORAGE_DIR, f"{DOMAIN}-{entry.unique_id}"),
|
||||||
|
)
|
||||||
|
@ -17,10 +17,11 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
|||||||
SchemaOptionsFlowHandler,
|
SchemaOptionsFlowHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN
|
from .const import CONF_RADAR_UPDATES, CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN
|
||||||
|
|
||||||
OPTIONS_SCHEMA = vol.Schema(
|
OPTIONS_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
|
vol.Required(CONF_RADAR_UPDATES, default=False): bool,
|
||||||
vol.Required(CONF_STATION_UPDATES, default=True): bool,
|
vol.Required(CONF_STATION_UPDATES, default=True): bool,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -51,8 +51,9 @@ from homeassistant.components.weather import (
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
ATTRIBUTION = "Powered by AEMET OpenData"
|
ATTRIBUTION = "Powered by AEMET OpenData"
|
||||||
|
CONF_RADAR_UPDATES = "radar_updates"
|
||||||
CONF_STATION_UPDATES = "station_updates"
|
CONF_STATION_UPDATES = "station_updates"
|
||||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
PLATFORMS = [Platform.IMAGE, Platform.SENSOR, Platform.WEATHER]
|
||||||
DEFAULT_NAME = "AEMET"
|
DEFAULT_NAME = "AEMET"
|
||||||
DOMAIN = "aemet"
|
DOMAIN = "aemet"
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aemet_opendata.const import AOD_COORDS
|
from aemet_opendata.const import AOD_COORDS, AOD_IMG_BYTES
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -26,6 +26,7 @@ TO_REDACT_CONFIG = [
|
|||||||
|
|
||||||
TO_REDACT_COORD = [
|
TO_REDACT_COORD = [
|
||||||
AOD_COORDS,
|
AOD_COORDS,
|
||||||
|
AOD_IMG_BYTES,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
86
homeassistant/components/aemet/image.py
Normal file
86
homeassistant/components/aemet/image.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
"""Support for the AEMET OpenData images."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from aemet_opendata.const import AOD_DATETIME, AOD_IMG_BYTES, AOD_IMG_TYPE, AOD_RADAR
|
||||||
|
from aemet_opendata.helpers import dict_nested_value
|
||||||
|
|
||||||
|
from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator
|
||||||
|
from .entity import AemetEntity
|
||||||
|
|
||||||
|
AEMET_IMAGES: Final[tuple[ImageEntityDescription, ...]] = (
|
||||||
|
ImageEntityDescription(
|
||||||
|
key=AOD_RADAR,
|
||||||
|
translation_key="weather_radar",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AemetConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up AEMET OpenData image entities based on a config entry."""
|
||||||
|
domain_data = config_entry.runtime_data
|
||||||
|
name = domain_data.name
|
||||||
|
coordinator = domain_data.coordinator
|
||||||
|
|
||||||
|
unique_id = config_entry.unique_id
|
||||||
|
assert unique_id is not None
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
AemetImage(
|
||||||
|
hass,
|
||||||
|
name,
|
||||||
|
coordinator,
|
||||||
|
description,
|
||||||
|
unique_id,
|
||||||
|
)
|
||||||
|
for description in AEMET_IMAGES
|
||||||
|
if dict_nested_value(coordinator.data["lib"], [description.key]) is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AemetImage(AemetEntity, ImageEntity):
|
||||||
|
"""Implementation of an AEMET OpenData image."""
|
||||||
|
|
||||||
|
entity_description: ImageEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
name: str,
|
||||||
|
coordinator: WeatherUpdateCoordinator,
|
||||||
|
description: ImageEntityDescription,
|
||||||
|
unique_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the image."""
|
||||||
|
super().__init__(coordinator, name, unique_id)
|
||||||
|
ImageEntity.__init__(self, hass)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{unique_id}-{description.key}"
|
||||||
|
|
||||||
|
self._async_update_attrs()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Update attributes when the coordinator updates."""
|
||||||
|
self._async_update_attrs()
|
||||||
|
super()._handle_coordinator_update()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update image attributes."""
|
||||||
|
image_data = self.get_aemet_value([self.entity_description.key])
|
||||||
|
self._cached_image = Image(
|
||||||
|
content_type=image_data.get(AOD_IMG_TYPE),
|
||||||
|
content=image_data.get(AOD_IMG_BYTES),
|
||||||
|
)
|
||||||
|
self._attr_image_last_updated = image_data.get(AOD_DATETIME)
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aemet_opendata"],
|
"loggers": ["aemet_opendata"],
|
||||||
"requirements": ["AEMET-OpenData==0.6.3"]
|
"requirements": ["AEMET-OpenData==0.6.4"]
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"entity": {
|
||||||
|
"image": {
|
||||||
|
"weather_radar": {
|
||||||
|
"name": "Weather radar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"radar_updates": "Gather data from AEMET weather radar",
|
||||||
"station_updates": "Gather data from AEMET weather stations"
|
"station_updates": "Gather data from AEMET weather stations"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from . import AirGradientConfigEntry
|
from . import AirGradientConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AirGradientCoordinator
|
from .coordinator import AirGradientCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity, exception_handler
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
@ -100,6 +102,7 @@ class AirGradientButton(AirGradientEntity, ButtonEntity):
|
|||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||||
|
|
||||||
|
@exception_handler
|
||||||
async def async_press(self) -> None:
|
async def async_press(self) -> None:
|
||||||
"""Press the button."""
|
"""Press the button."""
|
||||||
await self.entity_description.press_fn(self.coordinator.client)
|
await self.entity_description.press_fn(self.coordinator.client)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Config flow for Airgradient."""
|
"""Config flow for Airgradient."""
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from airgradient import (
|
from airgradient import (
|
||||||
@ -11,10 +12,15 @@ from airgradient import (
|
|||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import zeroconf
|
from homeassistant.config_entries import (
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
SOURCE_RECONFIGURE,
|
||||||
|
SOURCE_USER,
|
||||||
|
ConfigFlow,
|
||||||
|
ConfigFlowResult,
|
||||||
|
)
|
||||||
from homeassistant.const import CONF_HOST, CONF_MODEL
|
from homeassistant.const import CONF_HOST, CONF_MODEL
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
@ -37,7 +43,7 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
await self.client.set_configuration_control(ConfigurationControl.LOCAL)
|
await self.client.set_configuration_control(ConfigurationControl.LOCAL)
|
||||||
|
|
||||||
async def async_step_zeroconf(
|
async def async_step_zeroconf(
|
||||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
self, discovery_info: ZeroconfServiceInfo
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle zeroconf discovery."""
|
"""Handle zeroconf discovery."""
|
||||||
self.data[CONF_HOST] = host = discovery_info.host
|
self.data[CONF_HOST] = host = discovery_info.host
|
||||||
@ -95,10 +101,18 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
await self.async_set_unique_id(
|
await self.async_set_unique_id(
|
||||||
current_measures.serial_number, raise_on_progress=False
|
current_measures.serial_number, raise_on_progress=False
|
||||||
)
|
)
|
||||||
self._abort_if_unique_id_configured()
|
if self.source == SOURCE_USER:
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
if self.source == SOURCE_RECONFIGURE:
|
||||||
|
self._abort_if_unique_id_mismatch()
|
||||||
await self.set_configuration_source()
|
await self.set_configuration_source()
|
||||||
return self.async_create_entry(
|
if self.source == SOURCE_USER:
|
||||||
title=current_measures.model,
|
return self.async_create_entry(
|
||||||
|
title=current_measures.model,
|
||||||
|
data={CONF_HOST: user_input[CONF_HOST]},
|
||||||
|
)
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
self._get_reconfigure_entry(),
|
||||||
data={CONF_HOST: user_input[CONF_HOST]},
|
data={CONF_HOST: user_input[CONF_HOST]},
|
||||||
)
|
)
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
@ -106,3 +120,9 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_reconfigure(
|
||||||
|
self, user_input: Mapping[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle reconfiguration."""
|
||||||
|
return await self.async_step_user()
|
||||||
|
@ -55,7 +55,11 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
|||||||
measures = await self.client.get_current_measures()
|
measures = await self.client.get_current_measures()
|
||||||
config = await self.client.get_config()
|
config = await self.client.get_config()
|
||||||
except AirGradientError as error:
|
except AirGradientError as error:
|
||||||
raise UpdateFailed(error) from error
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="update_error",
|
||||||
|
translation_placeholders={"error": str(error)},
|
||||||
|
) from error
|
||||||
if measures.firmware_version != self._current_version:
|
if measures.firmware_version != self._current_version:
|
||||||
device_registry = dr.async_get(self.hass)
|
device_registry = dr.async_get(self.hass)
|
||||||
device_entry = device_registry.async_get_device(
|
device_entry = device_registry.async_get_device(
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
"""Base class for AirGradient entities."""
|
"""Base class for AirGradient entities."""
|
||||||
|
|
||||||
from airgradient import get_model_name
|
from collections.abc import Callable, Coroutine
|
||||||
|
from typing import Any, Concatenate
|
||||||
|
|
||||||
|
from airgradient import AirGradientConnectionError, AirGradientError, get_model_name
|
||||||
|
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
@ -26,3 +30,31 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
|
|||||||
serial_number=coordinator.serial_number,
|
serial_number=coordinator.serial_number,
|
||||||
sw_version=measures.firmware_version,
|
sw_version=measures.firmware_version,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def exception_handler[_EntityT: AirGradientEntity, **_P](
|
||||||
|
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
|
||||||
|
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
|
||||||
|
"""Decorate AirGradient calls to handle exceptions.
|
||||||
|
|
||||||
|
A decorator that wraps the passed in function, catches AirGradient errors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||||
|
try:
|
||||||
|
await func(self, *args, **kwargs)
|
||||||
|
except AirGradientConnectionError as error:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="communication_error",
|
||||||
|
translation_placeholders={"error": str(error)},
|
||||||
|
) from error
|
||||||
|
|
||||||
|
except AirGradientError as error:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="unknown_error",
|
||||||
|
translation_placeholders={"error": str(error)},
|
||||||
|
) from error
|
||||||
|
|
||||||
|
return handler
|
||||||
|
@ -19,7 +19,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from . import AirGradientConfigEntry
|
from . import AirGradientConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AirGradientCoordinator
|
from .coordinator import AirGradientCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity, exception_handler
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
@ -121,6 +123,7 @@ class AirGradientNumber(AirGradientEntity, NumberEntity):
|
|||||||
"""Return the state of the number."""
|
"""Return the state of the number."""
|
||||||
return self.entity_description.value_fn(self.coordinator.data.config)
|
return self.entity_description.value_fn(self.coordinator.data.config)
|
||||||
|
|
||||||
|
@exception_handler
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
"""Set the selected value."""
|
"""Set the selected value."""
|
||||||
await self.entity_description.set_value_fn(self.coordinator.client, int(value))
|
await self.entity_description.set_value_fn(self.coordinator.client, int(value))
|
||||||
|
@ -29,24 +29,30 @@ rules:
|
|||||||
unique-config-entry: done
|
unique-config-entry: done
|
||||||
|
|
||||||
# Silver
|
# Silver
|
||||||
action-exceptions: todo
|
action-exceptions: done
|
||||||
config-entry-unloading: done
|
config-entry-unloading: done
|
||||||
docs-configuration-parameters: todo
|
docs-configuration-parameters:
|
||||||
|
status: exempt
|
||||||
|
comment: No options to configure
|
||||||
docs-installation-parameters: todo
|
docs-installation-parameters: todo
|
||||||
entity-unavailable: done
|
entity-unavailable: done
|
||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
parallel-updates: todo
|
parallel-updates: done
|
||||||
reauthentication-flow:
|
reauthentication-flow:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: |
|
comment: |
|
||||||
This integration does not require authentication.
|
This integration does not require authentication.
|
||||||
test-coverage: done
|
test-coverage: todo
|
||||||
# Gold
|
# Gold
|
||||||
devices: done
|
devices: done
|
||||||
diagnostics: done
|
diagnostics: done
|
||||||
discovery-update-info: done
|
discovery-update-info:
|
||||||
discovery: done
|
status: todo
|
||||||
|
comment: DHCP is still possible
|
||||||
|
discovery:
|
||||||
|
status: todo
|
||||||
|
comment: DHCP is still possible
|
||||||
docs-data-update: todo
|
docs-data-update: todo
|
||||||
docs-examples: todo
|
docs-examples: todo
|
||||||
docs-known-limitations: todo
|
docs-known-limitations: todo
|
||||||
@ -62,9 +68,9 @@ rules:
|
|||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
entity-disabled-by-default: done
|
entity-disabled-by-default: done
|
||||||
entity-translations: done
|
entity-translations: done
|
||||||
exception-translations: todo
|
exception-translations: done
|
||||||
icon-translations: done
|
icon-translations: done
|
||||||
reconfiguration-flow: todo
|
reconfiguration-flow: done
|
||||||
repair-issues:
|
repair-issues:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: |
|
comment: |
|
||||||
|
@ -19,7 +19,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from . import AirGradientConfigEntry
|
from . import AirGradientConfigEntry
|
||||||
from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE
|
from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE
|
||||||
from .coordinator import AirGradientCoordinator
|
from .coordinator import AirGradientCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity, exception_handler
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
@ -216,6 +218,7 @@ class AirGradientSelect(AirGradientEntity, SelectEntity):
|
|||||||
"""Return the state of the select."""
|
"""Return the state of the select."""
|
||||||
return self.entity_description.value_fn(self.coordinator.data.config)
|
return self.entity_description.value_fn(self.coordinator.data.config)
|
||||||
|
|
||||||
|
@exception_handler
|
||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
"""Change the selected option."""
|
"""Change the selected option."""
|
||||||
await self.entity_description.set_value_fn(self.coordinator.client, option)
|
await self.entity_description.set_value_fn(self.coordinator.client, option)
|
||||||
|
@ -35,6 +35,8 @@ from .const import PM_STANDARD, PM_STANDARD_REVERSE
|
|||||||
from .coordinator import AirGradientCoordinator
|
from .coordinator import AirGradientCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AirGradientMeasurementSensorEntityDescription(SensorEntityDescription):
|
class AirGradientMeasurementSensorEntityDescription(SensorEntityDescription):
|
||||||
@ -137,6 +139,15 @@ MEASUREMENT_SENSOR_TYPES: tuple[AirGradientMeasurementSensorEntityDescription, .
|
|||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
value_fn=lambda status: status.raw_total_volatile_organic_component,
|
value_fn=lambda status: status.raw_total_volatile_organic_component,
|
||||||
),
|
),
|
||||||
|
AirGradientMeasurementSensorEntityDescription(
|
||||||
|
key="pm02_raw",
|
||||||
|
translation_key="raw_pm02",
|
||||||
|
device_class=SensorDeviceClass.PM25,
|
||||||
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
value_fn=lambda status: status.raw_pm02,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
CONFIG_SENSOR_TYPES: tuple[AirGradientConfigSensorEntityDescription, ...] = (
|
CONFIG_SENSOR_TYPES: tuple[AirGradientConfigSensorEntityDescription, ...] = (
|
||||||
|
@ -17,7 +17,9 @@
|
|||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||||
"invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1."
|
"invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1.",
|
||||||
|
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||||
|
"unique_id_mismatch": "Please ensure you reconfigure against the same device."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
@ -119,6 +121,9 @@
|
|||||||
"raw_nitrogen": {
|
"raw_nitrogen": {
|
||||||
"name": "Raw NOx"
|
"name": "Raw NOx"
|
||||||
},
|
},
|
||||||
|
"raw_pm02": {
|
||||||
|
"name": "Raw PM2.5"
|
||||||
|
},
|
||||||
"display_pm_standard": {
|
"display_pm_standard": {
|
||||||
"name": "[%key:component::airgradient::entity::select::display_pm_standard::name%]",
|
"name": "[%key:component::airgradient::entity::select::display_pm_standard::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
@ -162,5 +167,16 @@
|
|||||||
"name": "Post data to Airgradient"
|
"name": "Post data to Airgradient"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"communication_error": {
|
||||||
|
"message": "An error occurred while communicating with the Airgradient device: {error}"
|
||||||
|
},
|
||||||
|
"unknown_error": {
|
||||||
|
"message": "An unknown error occurred while communicating with the Airgradient device: {error}"
|
||||||
|
},
|
||||||
|
"update_error": {
|
||||||
|
"message": "An error occurred while communicating with the Airgradient device: {error}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from . import AirGradientConfigEntry
|
from . import AirGradientConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AirGradientCoordinator
|
from .coordinator import AirGradientCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity, exception_handler
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
@ -99,11 +101,13 @@ class AirGradientSwitch(AirGradientEntity, SwitchEntity):
|
|||||||
"""Return the state of the switch."""
|
"""Return the state of the switch."""
|
||||||
return self.entity_description.value_fn(self.coordinator.data.config)
|
return self.entity_description.value_fn(self.coordinator.data.config)
|
||||||
|
|
||||||
|
@exception_handler
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch on."""
|
"""Turn the switch on."""
|
||||||
await self.entity_description.set_value_fn(self.coordinator.client, True)
|
await self.entity_description.set_value_fn(self.coordinator.client, True)
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
@exception_handler
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch off."""
|
"""Turn the switch off."""
|
||||||
await self.entity_description.set_value_fn(self.coordinator.client, False)
|
await self.entity_description.set_value_fn(self.coordinator.client, False)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from propcache import cached_property
|
from propcache.api import cached_property
|
||||||
|
|
||||||
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -11,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from . import AirGradientConfigEntry, AirGradientCoordinator
|
from . import AirGradientConfigEntry, AirGradientCoordinator
|
||||||
from .entity import AirGradientEntity
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
SCAN_INTERVAL = timedelta(hours=1)
|
SCAN_INTERVAL = timedelta(hours=1)
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ from .const import (
|
|||||||
ATTR_API_CAT_DESCRIPTION,
|
ATTR_API_CAT_DESCRIPTION,
|
||||||
ATTR_API_CAT_LEVEL,
|
ATTR_API_CAT_LEVEL,
|
||||||
ATTR_API_CATEGORY,
|
ATTR_API_CATEGORY,
|
||||||
ATTR_API_PM25,
|
|
||||||
ATTR_API_POLLUTANT,
|
ATTR_API_POLLUTANT,
|
||||||
ATTR_API_REPORT_DATE,
|
ATTR_API_REPORT_DATE,
|
||||||
ATTR_API_REPORT_HOUR,
|
ATTR_API_REPORT_HOUR,
|
||||||
@ -91,18 +90,16 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
max_aqi_desc = obv[ATTR_API_CATEGORY][ATTR_API_CAT_DESCRIPTION]
|
max_aqi_desc = obv[ATTR_API_CATEGORY][ATTR_API_CAT_DESCRIPTION]
|
||||||
max_aqi_poll = pollutant
|
max_aqi_poll = pollutant
|
||||||
|
|
||||||
# Copy other data from PM2.5 Value
|
# Copy Report Details
|
||||||
if obv[ATTR_API_AQI_PARAM] == ATTR_API_PM25:
|
data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE]
|
||||||
# Copy Report Details
|
data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR]
|
||||||
data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE]
|
data[ATTR_API_REPORT_TZ] = obv[ATTR_API_REPORT_TZ]
|
||||||
data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR]
|
|
||||||
data[ATTR_API_REPORT_TZ] = obv[ATTR_API_REPORT_TZ]
|
|
||||||
|
|
||||||
# Copy Station Details
|
# Copy Station Details
|
||||||
data[ATTR_API_STATE] = obv[ATTR_API_STATE]
|
data[ATTR_API_STATE] = obv[ATTR_API_STATE]
|
||||||
data[ATTR_API_STATION] = obv[ATTR_API_STATION]
|
data[ATTR_API_STATION] = obv[ATTR_API_STATION]
|
||||||
data[ATTR_API_STATION_LATITUDE] = obv[ATTR_API_STATION_LATITUDE]
|
data[ATTR_API_STATION_LATITUDE] = obv[ATTR_API_STATION_LATITUDE]
|
||||||
data[ATTR_API_STATION_LONGITUDE] = obv[ATTR_API_STATION_LONGITUDE]
|
data[ATTR_API_STATION_LONGITUDE] = obv[ATTR_API_STATION_LONGITUDE]
|
||||||
|
|
||||||
# Store Overall AQI
|
# Store Overall AQI
|
||||||
data[ATTR_API_AQI] = max_aqi
|
data[ATTR_API_AQI] = max_aqi
|
||||||
|
@ -39,45 +39,54 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
|||||||
key="temp",
|
key="temp",
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
"humidity": SensorEntityDescription(
|
"humidity": SensorEntityDescription(
|
||||||
key="humidity",
|
key="humidity",
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
"pressure": SensorEntityDescription(
|
"pressure": SensorEntityDescription(
|
||||||
key="pressure",
|
key="pressure",
|
||||||
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||||
native_unit_of_measurement=UnitOfPressure.MBAR,
|
native_unit_of_measurement=UnitOfPressure.MBAR,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
"battery": SensorEntityDescription(
|
"battery": SensorEntityDescription(
|
||||||
key="battery",
|
key="battery",
|
||||||
device_class=SensorDeviceClass.BATTERY,
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
"co2": SensorEntityDescription(
|
"co2": SensorEntityDescription(
|
||||||
key="co2",
|
key="co2",
|
||||||
device_class=SensorDeviceClass.CO2,
|
device_class=SensorDeviceClass.CO2,
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
"voc": SensorEntityDescription(
|
"voc": SensorEntityDescription(
|
||||||
key="voc",
|
key="voc",
|
||||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
"light": SensorEntityDescription(
|
"light": SensorEntityDescription(
|
||||||
key="light",
|
key="light",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
translation_key="light",
|
translation_key="light",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
"virusRisk": SensorEntityDescription(
|
"virusRisk": SensorEntityDescription(
|
||||||
key="virusRisk",
|
key="virusRisk",
|
||||||
translation_key="virus_risk",
|
translation_key="virus_risk",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
"mold": SensorEntityDescription(
|
"mold": SensorEntityDescription(
|
||||||
key="mold",
|
key="mold",
|
||||||
translation_key="mold",
|
translation_key="mold",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
"rssi": SensorEntityDescription(
|
"rssi": SensorEntityDescription(
|
||||||
key="rssi",
|
key="rssi",
|
||||||
@ -85,16 +94,19 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
|||||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
"pm1": SensorEntityDescription(
|
"pm1": SensorEntityDescription(
|
||||||
key="pm1",
|
key="pm1",
|
||||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
device_class=SensorDeviceClass.PM1,
|
device_class=SensorDeviceClass.PM1,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
"pm25": SensorEntityDescription(
|
"pm25": SensorEntityDescription(
|
||||||
key="pm25",
|
key="pm25",
|
||||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
device_class=SensorDeviceClass.PM25,
|
device_class=SensorDeviceClass.PM25,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,8 +155,7 @@ class AirthingsHeaterEnergySensor(
|
|||||||
self._id = airthings_device.device_id
|
self._id = airthings_device.device_id
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
configuration_url=(
|
configuration_url=(
|
||||||
"https://dashboard.airthings.com/devices/"
|
f"https://dashboard.airthings.com/devices/{airthings_device.device_id}"
|
||||||
f"{airthings_device.device_id}"
|
|
||||||
),
|
),
|
||||||
identifiers={(DOMAIN, airthings_device.device_id)},
|
identifiers={(DOMAIN, airthings_device.device_id)},
|
||||||
name=airthings_device.name,
|
name=airthings_device.name,
|
||||||
|
@ -67,18 +67,21 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
|||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=1,
|
||||||
),
|
),
|
||||||
"humidity": SensorEntityDescription(
|
"humidity": SensorEntityDescription(
|
||||||
key="humidity",
|
key="humidity",
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=1,
|
||||||
),
|
),
|
||||||
"pressure": SensorEntityDescription(
|
"pressure": SensorEntityDescription(
|
||||||
key="pressure",
|
key="pressure",
|
||||||
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||||
native_unit_of_measurement=UnitOfPressure.MBAR,
|
native_unit_of_measurement=UnitOfPressure.MBAR,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=1,
|
||||||
),
|
),
|
||||||
"battery": SensorEntityDescription(
|
"battery": SensorEntityDescription(
|
||||||
key="battery",
|
key="battery",
|
||||||
@ -86,24 +89,28 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
|||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
suggested_display_precision=0,
|
||||||
),
|
),
|
||||||
"co2": SensorEntityDescription(
|
"co2": SensorEntityDescription(
|
||||||
key="co2",
|
key="co2",
|
||||||
device_class=SensorDeviceClass.CO2,
|
device_class=SensorDeviceClass.CO2,
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
),
|
),
|
||||||
"voc": SensorEntityDescription(
|
"voc": SensorEntityDescription(
|
||||||
key="voc",
|
key="voc",
|
||||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
),
|
),
|
||||||
"illuminance": SensorEntityDescription(
|
"illuminance": SensorEntityDescription(
|
||||||
key="illuminance",
|
key="illuminance",
|
||||||
translation_key="illuminance",
|
translation_key="illuminance",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +95,6 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
|
|||||||
| ClimateEntityFeature.TURN_ON
|
| ClimateEntityFeature.TURN_ON
|
||||||
)
|
)
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
_enable_turn_on_off_backwards_compatibility = False
|
|
||||||
|
|
||||||
def __init__(self, coordinator, ac_number, info):
|
def __init__(self, coordinator, ac_number, info):
|
||||||
"""Initialize the climate device."""
|
"""Initialize the climate device."""
|
||||||
@ -205,7 +204,6 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
|
|||||||
)
|
)
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
_attr_hvac_modes = AT_GROUP_MODES
|
_attr_hvac_modes = AT_GROUP_MODES
|
||||||
_enable_turn_on_off_backwards_compatibility = False
|
|
||||||
|
|
||||||
def __init__(self, coordinator, group_number, info):
|
def __init__(self, coordinator, group_number, info):
|
||||||
"""Initialize the climate device."""
|
"""Initialize the climate device."""
|
||||||
|
@ -124,7 +124,6 @@ class Airtouch5ClimateEntity(ClimateEntity, Airtouch5Entity):
|
|||||||
_attr_translation_key = DOMAIN
|
_attr_translation_key = DOMAIN
|
||||||
_attr_target_temperature_step = 1
|
_attr_target_temperature_step = 1
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
_enable_turn_on_off_backwards_compatibility = False
|
|
||||||
|
|
||||||
|
|
||||||
class Airtouch5AC(Airtouch5ClimateEntity):
|
class Airtouch5AC(Airtouch5ClimateEntity):
|
||||||
|
@ -50,7 +50,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value_fn=lambda settings, status, measurements, history: int(
|
value_fn=lambda settings, status, measurements, history: int(
|
||||||
history.get(
|
history.get(
|
||||||
f'Outdoor {"AQI(US)" if settings["is_aqi_usa"] else "AQI(CN)"}', -1
|
f"Outdoor {'AQI(US)' if settings['is_aqi_usa'] else 'AQI(CN)'}", -1
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
translation_key="outdoor_air_quality_index",
|
translation_key="outdoor_air_quality_index",
|
||||||
|
@ -5,7 +5,14 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aioairzone.const import AZD_MAC, AZD_WEBSERVER, DEFAULT_SYSTEM_ID
|
from aioairzone.const import (
|
||||||
|
AZD_FIRMWARE,
|
||||||
|
AZD_FULL_NAME,
|
||||||
|
AZD_MAC,
|
||||||
|
AZD_MODEL,
|
||||||
|
AZD_WEBSERVER,
|
||||||
|
DEFAULT_SYSTEM_ID,
|
||||||
|
)
|
||||||
from aioairzone.localapi import AirzoneLocalApi, ConnectionOptions
|
from aioairzone.localapi import AirzoneLocalApi, ConnectionOptions
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -17,6 +24,7 @@ from homeassistant.helpers import (
|
|||||||
entity_registry as er,
|
entity_registry as er,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN, MANUFACTURER
|
||||||
from .coordinator import AirzoneUpdateCoordinator
|
from .coordinator import AirzoneUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [
|
PLATFORMS: list[Platform] = [
|
||||||
@ -78,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> b
|
|||||||
options = ConnectionOptions(
|
options = ConnectionOptions(
|
||||||
entry.data[CONF_HOST],
|
entry.data[CONF_HOST],
|
||||||
entry.data[CONF_PORT],
|
entry.data[CONF_PORT],
|
||||||
entry.data.get(CONF_ID, DEFAULT_SYSTEM_ID),
|
entry.data[CONF_ID],
|
||||||
)
|
)
|
||||||
|
|
||||||
airzone = AirzoneLocalApi(aiohttp_client.async_get_clientsession(hass), options)
|
airzone = AirzoneLocalApi(aiohttp_client.async_get_clientsession(hass), options)
|
||||||
@ -88,6 +96,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> b
|
|||||||
|
|
||||||
entry.runtime_data = coordinator
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
|
ws_data: dict[str, Any] | None = coordinator.data.get(AZD_WEBSERVER)
|
||||||
|
if ws_data is not None:
|
||||||
|
mac = ws_data.get(AZD_MAC, "")
|
||||||
|
|
||||||
|
device_registry.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
connections={(dr.CONNECTION_NETWORK_MAC, mac)},
|
||||||
|
identifiers={(DOMAIN, f"{entry.entry_id}_ws")},
|
||||||
|
manufacturer=MANUFACTURER,
|
||||||
|
model=ws_data.get(AZD_MODEL),
|
||||||
|
name=ws_data.get(AZD_FULL_NAME),
|
||||||
|
sw_version=ws_data.get(AZD_FIRMWARE),
|
||||||
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -96,3 +120,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> b
|
|||||||
async def async_unload_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> bool:
|
||||||
|
"""Migrate an old entry."""
|
||||||
|
if entry.version == 1 and entry.minor_version < 2:
|
||||||
|
# Add missing CONF_ID
|
||||||
|
system_id = entry.data.get(CONF_ID, DEFAULT_SYSTEM_ID)
|
||||||
|
new_data = entry.data.copy()
|
||||||
|
new_data[CONF_ID] = system_id
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry,
|
||||||
|
data=new_data,
|
||||||
|
minor_version=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.info(
|
||||||
|
"Migration to configuration version %s.%s successful",
|
||||||
|
entry.version,
|
||||||
|
entry.minor_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
@ -136,7 +136,6 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
|||||||
_attr_name = None
|
_attr_name = None
|
||||||
_speeds: dict[int, str] = {}
|
_speeds: dict[int, str] = {}
|
||||||
_speeds_reverse: dict[str, int] = {}
|
_speeds_reverse: dict[str, int] = {}
|
||||||
_enable_turn_on_off_backwards_compatibility = False
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -10,12 +10,12 @@ from aioairzone.exceptions import AirzoneError, InvalidSystem
|
|||||||
from aioairzone.localapi import AirzoneLocalApi, ConnectionOptions
|
from aioairzone.localapi import AirzoneLocalApi, ConnectionOptions
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import dhcp
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT
|
||||||
from homeassistant.data_entry_flow import AbortFlow
|
from homeassistant.data_entry_flow import AbortFlow
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
|
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
@ -44,6 +44,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
_discovered_ip: str | None = None
|
_discovered_ip: str | None = None
|
||||||
_discovered_mac: str | None = None
|
_discovered_mac: str | None = None
|
||||||
|
MINOR_VERSION = 2
|
||||||
|
|
||||||
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
|
||||||
@ -53,6 +54,9 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
|
if CONF_ID not in user_input:
|
||||||
|
user_input[CONF_ID] = DEFAULT_SYSTEM_ID
|
||||||
|
|
||||||
self._async_abort_entries_match(user_input)
|
self._async_abort_entries_match(user_input)
|
||||||
|
|
||||||
airzone = AirzoneLocalApi(
|
airzone = AirzoneLocalApi(
|
||||||
@ -60,7 +64,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
ConnectionOptions(
|
ConnectionOptions(
|
||||||
user_input[CONF_HOST],
|
user_input[CONF_HOST],
|
||||||
user_input[CONF_PORT],
|
user_input[CONF_PORT],
|
||||||
user_input.get(CONF_ID, DEFAULT_SYSTEM_ID),
|
user_input[CONF_ID],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -84,6 +88,9 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
title = f"Airzone {user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
|
title = f"Airzone {user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
|
||||||
|
if user_input[CONF_ID] != DEFAULT_SYSTEM_ID:
|
||||||
|
title += f" #{user_input[CONF_ID]}"
|
||||||
|
|
||||||
return self.async_create_entry(title=title, data=user_input)
|
return self.async_create_entry(title=title, data=user_input)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
@ -93,7 +100,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_dhcp(
|
async def async_step_dhcp(
|
||||||
self, discovery_info: dhcp.DhcpServiceInfo
|
self, discovery_info: DhcpServiceInfo
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle DHCP discovery."""
|
"""Handle DHCP discovery."""
|
||||||
self._discovered_ip = discovery_info.ip
|
self._discovered_ip = discovery_info.ip
|
||||||
|
@ -68,8 +68,9 @@ class AirzoneSystemEntity(AirzoneEntity):
|
|||||||
model=self.get_airzone_value(AZD_MODEL),
|
model=self.get_airzone_value(AZD_MODEL),
|
||||||
name=f"System {self.system_id}",
|
name=f"System {self.system_id}",
|
||||||
sw_version=self.get_airzone_value(AZD_FIRMWARE),
|
sw_version=self.get_airzone_value(AZD_FIRMWARE),
|
||||||
via_device=(DOMAIN, f"{entry.entry_id}_ws"),
|
|
||||||
)
|
)
|
||||||
|
if AZD_WEBSERVER in self.coordinator.data:
|
||||||
|
self._attr_device_info["via_device"] = (DOMAIN, f"{entry.entry_id}_ws")
|
||||||
self._attr_unique_id = entry.unique_id or entry.entry_id
|
self._attr_unique_id = entry.unique_id or entry.entry_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -102,8 +103,9 @@ class AirzoneHotWaterEntity(AirzoneEntity):
|
|||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
model="DHW",
|
model="DHW",
|
||||||
name=self.get_airzone_value(AZD_NAME),
|
name=self.get_airzone_value(AZD_NAME),
|
||||||
via_device=(DOMAIN, f"{entry.entry_id}_ws"),
|
|
||||||
)
|
)
|
||||||
|
if AZD_WEBSERVER in self.coordinator.data:
|
||||||
|
self._attr_device_info["via_device"] = (DOMAIN, f"{entry.entry_id}_ws")
|
||||||
self._attr_unique_id = entry.unique_id or entry.entry_id
|
self._attr_unique_id = entry.unique_id or entry.entry_id
|
||||||
|
|
||||||
def get_airzone_value(self, key: str) -> Any:
|
def get_airzone_value(self, key: str) -> Any:
|
||||||
|
@ -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.7"]
|
"requirements": ["aioairzone==0.9.9"]
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,6 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity):
|
|||||||
|
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
_enable_turn_on_off_backwards_compatibility = False
|
|
||||||
|
|
||||||
def _init_attributes(self) -> None:
|
def _init_attributes(self) -> None:
|
||||||
"""Init common climate device attributes."""
|
"""Init common climate device attributes."""
|
||||||
@ -194,12 +193,6 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity):
|
|||||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
|
||||||
self.get_airzone_value(AZD_SPEED) is not None
|
|
||||||
and self.get_airzone_value(AZD_SPEEDS) is not None
|
|
||||||
):
|
|
||||||
self._initialize_fan_speeds()
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Update attributes when the coordinator updates."""
|
"""Update attributes when the coordinator updates."""
|
||||||
@ -214,8 +207,6 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity):
|
|||||||
self._attr_hvac_action = HVAC_ACTION_LIB_TO_HASS[
|
self._attr_hvac_action = HVAC_ACTION_LIB_TO_HASS[
|
||||||
self.get_airzone_value(AZD_ACTION)
|
self.get_airzone_value(AZD_ACTION)
|
||||||
]
|
]
|
||||||
if self.supported_features & ClimateEntityFeature.FAN_MODE:
|
|
||||||
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
|
|
||||||
if self.get_airzone_value(AZD_POWER):
|
if self.get_airzone_value(AZD_POWER):
|
||||||
self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[
|
self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[
|
||||||
self.get_airzone_value(AZD_MODE)
|
self.get_airzone_value(AZD_MODE)
|
||||||
@ -252,6 +243,22 @@ class AirzoneDeviceClimate(AirzoneClimate):
|
|||||||
_speeds: dict[int, str]
|
_speeds: dict[int, str]
|
||||||
_speeds_reverse: dict[str, int]
|
_speeds_reverse: dict[str, int]
|
||||||
|
|
||||||
|
def _init_attributes(self) -> None:
|
||||||
|
"""Init common climate device attributes."""
|
||||||
|
super()._init_attributes()
|
||||||
|
if (
|
||||||
|
self.get_airzone_value(AZD_SPEED) is not None
|
||||||
|
and self.get_airzone_value(AZD_SPEEDS) is not None
|
||||||
|
):
|
||||||
|
self._initialize_fan_speeds()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update climate attributes."""
|
||||||
|
super()._async_update_attrs()
|
||||||
|
if self.supported_features & ClimateEntityFeature.FAN_MODE:
|
||||||
|
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
|
||||||
|
|
||||||
def _initialize_fan_speeds(self) -> None:
|
def _initialize_fan_speeds(self) -> None:
|
||||||
"""Initialize fan speeds."""
|
"""Initialize fan speeds."""
|
||||||
azd_speeds: dict[int, int] = self.get_airzone_value(AZD_SPEEDS)
|
azd_speeds: dict[int, int] = self.get_airzone_value(AZD_SPEEDS)
|
||||||
|
@ -4,11 +4,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import partial
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Final, final
|
from typing import TYPE_CHECKING, Any, Final, final
|
||||||
|
|
||||||
from propcache import cached_property
|
from propcache.api import cached_property
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -27,26 +26,14 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.config_validation import make_entity_service_schema
|
from homeassistant.helpers.config_validation import make_entity_service_schema
|
||||||
from homeassistant.helpers.deprecation import (
|
|
||||||
all_with_deprecated_constants,
|
|
||||||
check_if_deprecated_constant,
|
|
||||||
dir_with_deprecated_constants,
|
|
||||||
)
|
|
||||||
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.entity_platform import EntityPlatform
|
||||||
|
from homeassistant.helpers.frame import ReportBehavior, report_usage
|
||||||
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
|
||||||
|
|
||||||
from .const import ( # noqa: F401
|
from .const import (
|
||||||
_DEPRECATED_FORMAT_NUMBER,
|
|
||||||
_DEPRECATED_FORMAT_TEXT,
|
|
||||||
_DEPRECATED_SUPPORT_ALARM_ARM_AWAY,
|
|
||||||
_DEPRECATED_SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
|
|
||||||
_DEPRECATED_SUPPORT_ALARM_ARM_HOME,
|
|
||||||
_DEPRECATED_SUPPORT_ALARM_ARM_NIGHT,
|
|
||||||
_DEPRECATED_SUPPORT_ALARM_ARM_VACATION,
|
|
||||||
_DEPRECATED_SUPPORT_ALARM_TRIGGER,
|
|
||||||
ATTR_CHANGED_BY,
|
ATTR_CHANGED_BY,
|
||||||
ATTR_CODE_ARM_REQUIRED,
|
ATTR_CODE_ARM_REQUIRED,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -163,7 +150,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: bool = False
|
||||||
__alarm_legacy_state_reported: bool = False
|
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||||
"""Post initialisation processing."""
|
"""Post initialisation processing."""
|
||||||
@ -180,9 +166,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
|||||||
unless already reported.
|
unless already reported.
|
||||||
"""
|
"""
|
||||||
if name == "_attr_state":
|
if name == "_attr_state":
|
||||||
if self.__alarm_legacy_state_reported is not True:
|
self._report_deprecated_alarm_state_handling()
|
||||||
self._report_deprecated_alarm_state_handling()
|
|
||||||
self.__alarm_legacy_state_reported = True
|
|
||||||
return super().__setattr__(name, value)
|
return super().__setattr__(name, value)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -194,7 +178,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Start adding an entity to a platform."""
|
"""Start adding an entity to a platform."""
|
||||||
super().add_to_platform_start(hass, platform, parallel_updates)
|
super().add_to_platform_start(hass, platform, parallel_updates)
|
||||||
if self.__alarm_legacy_state and not self.__alarm_legacy_state_reported:
|
if self.__alarm_legacy_state:
|
||||||
self._report_deprecated_alarm_state_handling()
|
self._report_deprecated_alarm_state_handling()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -203,19 +187,16 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
|||||||
|
|
||||||
Integrations should implement alarm_state instead of using state directly.
|
Integrations should implement alarm_state instead of using state directly.
|
||||||
"""
|
"""
|
||||||
self.__alarm_legacy_state_reported = True
|
report_usage(
|
||||||
if "custom_components" in type(self).__module__:
|
"is setting state directly."
|
||||||
# Do not report on core integrations as they have been fixed.
|
f" Entity {self.entity_id} ({type(self)}) should implement the 'alarm_state'"
|
||||||
report_issue = "report it to the custom integration author."
|
" property and return its state using the AlarmControlPanelState enum",
|
||||||
_LOGGER.warning(
|
core_integration_behavior=ReportBehavior.ERROR,
|
||||||
"Entity %s (%s) is setting state directly"
|
custom_integration_behavior=ReportBehavior.LOG,
|
||||||
" which will stop working in HA Core 2025.11."
|
breaks_in_ha_version="2025.11",
|
||||||
" Entities should implement the 'alarm_state' property and"
|
integration_domain=self.platform.platform_name if self.platform else None,
|
||||||
" return its state using the AlarmControlPanelState enum, please %s",
|
exclude_integrations={DOMAIN},
|
||||||
self.entity_id,
|
)
|
||||||
type(self),
|
|
||||||
report_issue,
|
|
||||||
)
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@property
|
@property
|
||||||
@ -374,12 +355,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
|||||||
@cached_property
|
@cached_property
|
||||||
def supported_features(self) -> AlarmControlPanelEntityFeature:
|
def supported_features(self) -> AlarmControlPanelEntityFeature:
|
||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
features = self._attr_supported_features
|
return self._attr_supported_features
|
||||||
if type(features) is int: # noqa: E721
|
|
||||||
new_features = AlarmControlPanelEntityFeature(features)
|
|
||||||
self._report_deprecated_supported_features_values(new_features)
|
|
||||||
return new_features
|
|
||||||
return features
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@property
|
@property
|
||||||
@ -417,13 +393,3 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
|||||||
self._alarm_control_panel_option_default_code = default_code
|
self._alarm_control_panel_option_default_code = default_code
|
||||||
return
|
return
|
||||||
self._alarm_control_panel_option_default_code = None
|
self._alarm_control_panel_option_default_code = None
|
||||||
|
|
||||||
|
|
||||||
# As we import constants of the const module here, we need to add the following
|
|
||||||
# functions to check for deprecated constants again
|
|
||||||
# These can be removed if no deprecated constant are in this module anymore
|
|
||||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
|
||||||
__dir__ = partial(
|
|
||||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
|
||||||
)
|
|
||||||
__all__ = all_with_deprecated_constants(globals())
|
|
||||||
|
@ -1,16 +1,8 @@
|
|||||||
"""Provides the constants needed for component."""
|
"""Provides the constants needed for component."""
|
||||||
|
|
||||||
from enum import IntFlag, StrEnum
|
from enum import IntFlag, StrEnum
|
||||||
from functools import partial
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from homeassistant.helpers.deprecation import (
|
|
||||||
DeprecatedConstantEnum,
|
|
||||||
all_with_deprecated_constants,
|
|
||||||
check_if_deprecated_constant,
|
|
||||||
dir_with_deprecated_constants,
|
|
||||||
)
|
|
||||||
|
|
||||||
DOMAIN: Final = "alarm_control_panel"
|
DOMAIN: Final = "alarm_control_panel"
|
||||||
|
|
||||||
ATTR_CHANGED_BY: Final = "changed_by"
|
ATTR_CHANGED_BY: Final = "changed_by"
|
||||||
@ -39,12 +31,6 @@ class CodeFormat(StrEnum):
|
|||||||
NUMBER = "number"
|
NUMBER = "number"
|
||||||
|
|
||||||
|
|
||||||
# These constants are deprecated as of Home Assistant 2022.5, can be removed in 2025.1
|
|
||||||
# Please use the CodeFormat enum instead.
|
|
||||||
_DEPRECATED_FORMAT_TEXT: Final = DeprecatedConstantEnum(CodeFormat.TEXT, "2025.1")
|
|
||||||
_DEPRECATED_FORMAT_NUMBER: Final = DeprecatedConstantEnum(CodeFormat.NUMBER, "2025.1")
|
|
||||||
|
|
||||||
|
|
||||||
class AlarmControlPanelEntityFeature(IntFlag):
|
class AlarmControlPanelEntityFeature(IntFlag):
|
||||||
"""Supported features of the alarm control panel entity."""
|
"""Supported features of the alarm control panel entity."""
|
||||||
|
|
||||||
@ -56,27 +42,6 @@ class AlarmControlPanelEntityFeature(IntFlag):
|
|||||||
ARM_VACATION = 32
|
ARM_VACATION = 32
|
||||||
|
|
||||||
|
|
||||||
# These constants are deprecated as of Home Assistant 2022.5
|
|
||||||
# Please use the AlarmControlPanelEntityFeature enum instead.
|
|
||||||
_DEPRECATED_SUPPORT_ALARM_ARM_HOME: Final = DeprecatedConstantEnum(
|
|
||||||
AlarmControlPanelEntityFeature.ARM_HOME, "2025.1"
|
|
||||||
)
|
|
||||||
_DEPRECATED_SUPPORT_ALARM_ARM_AWAY: Final = DeprecatedConstantEnum(
|
|
||||||
AlarmControlPanelEntityFeature.ARM_AWAY, "2025.1"
|
|
||||||
)
|
|
||||||
_DEPRECATED_SUPPORT_ALARM_ARM_NIGHT: Final = DeprecatedConstantEnum(
|
|
||||||
AlarmControlPanelEntityFeature.ARM_NIGHT, "2025.1"
|
|
||||||
)
|
|
||||||
_DEPRECATED_SUPPORT_ALARM_TRIGGER: Final = DeprecatedConstantEnum(
|
|
||||||
AlarmControlPanelEntityFeature.TRIGGER, "2025.1"
|
|
||||||
)
|
|
||||||
_DEPRECATED_SUPPORT_ALARM_ARM_CUSTOM_BYPASS: Final = DeprecatedConstantEnum(
|
|
||||||
AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, "2025.1"
|
|
||||||
)
|
|
||||||
_DEPRECATED_SUPPORT_ALARM_ARM_VACATION: Final = DeprecatedConstantEnum(
|
|
||||||
AlarmControlPanelEntityFeature.ARM_VACATION, "2025.1"
|
|
||||||
)
|
|
||||||
|
|
||||||
CONDITION_TRIGGERED: Final = "is_triggered"
|
CONDITION_TRIGGERED: Final = "is_triggered"
|
||||||
CONDITION_DISARMED: Final = "is_disarmed"
|
CONDITION_DISARMED: Final = "is_disarmed"
|
||||||
CONDITION_ARMED_HOME: Final = "is_armed_home"
|
CONDITION_ARMED_HOME: Final = "is_armed_home"
|
||||||
@ -84,10 +49,3 @@ CONDITION_ARMED_AWAY: Final = "is_armed_away"
|
|||||||
CONDITION_ARMED_NIGHT: Final = "is_armed_night"
|
CONDITION_ARMED_NIGHT: Final = "is_armed_night"
|
||||||
CONDITION_ARMED_VACATION: Final = "is_armed_vacation"
|
CONDITION_ARMED_VACATION: Final = "is_armed_vacation"
|
||||||
CONDITION_ARMED_CUSTOM_BYPASS: Final = "is_armed_custom_bypass"
|
CONDITION_ARMED_CUSTOM_BYPASS: Final = "is_armed_custom_bypass"
|
||||||
|
|
||||||
# These can be removed if no deprecated constant are in this module anymore
|
|
||||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
|
||||||
__dir__ = partial(
|
|
||||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
|
||||||
)
|
|
||||||
__all__ = all_with_deprecated_constants(globals())
|
|
||||||
|
@ -317,6 +317,7 @@ class Alexa(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,6 +404,7 @@ class AlexaPowerController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,7 +438,7 @@ class AlexaPowerController(AlexaCapability):
|
|||||||
elif self.entity.domain == remote.DOMAIN:
|
elif self.entity.domain == remote.DOMAIN:
|
||||||
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
|
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
|
||||||
elif self.entity.domain == vacuum.DOMAIN:
|
elif self.entity.domain == vacuum.DOMAIN:
|
||||||
is_on = self.entity.state == vacuum.STATE_CLEANING
|
is_on = self.entity.state == vacuum.VacuumActivity.CLEANING
|
||||||
elif self.entity.domain == timer.DOMAIN:
|
elif self.entity.domain == timer.DOMAIN:
|
||||||
is_on = self.entity.state != STATE_IDLE
|
is_on = self.entity.state != STATE_IDLE
|
||||||
elif self.entity.domain == water_heater.DOMAIN:
|
elif self.entity.domain == water_heater.DOMAIN:
|
||||||
@ -469,6 +471,7 @@ class AlexaLockController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -523,6 +526,7 @@ class AlexaSceneController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -562,6 +566,7 @@ class AlexaBrightnessController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -611,6 +616,7 @@ class AlexaColorController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -669,6 +675,7 @@ class AlexaColorTemperatureController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -715,6 +722,7 @@ class AlexaSpeaker(AlexaCapability):
|
|||||||
"fr-FR", # Not documented as of 2021-12-04, see PR #60489
|
"fr-FR", # Not documented as of 2021-12-04, see PR #60489
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
}
|
}
|
||||||
|
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@ -772,6 +780,7 @@ class AlexaStepSpeaker(AlexaCapability):
|
|||||||
"es-ES",
|
"es-ES",
|
||||||
"fr-FR", # Not documented as of 2021-12-04, see PR #60489
|
"fr-FR", # Not documented as of 2021-12-04, see PR #60489
|
||||||
"it-IT",
|
"it-IT",
|
||||||
|
"nl-NL",
|
||||||
}
|
}
|
||||||
|
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@ -801,6 +810,7 @@ class AlexaPlaybackController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -859,6 +869,7 @@ class AlexaInputController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1104,6 +1115,7 @@ class AlexaThermostatController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1245,6 +1257,7 @@ class AlexaPowerLevelController(AlexaCapability):
|
|||||||
"fr-CA",
|
"fr-CA",
|
||||||
"fr-FR",
|
"fr-FR",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
|
"nl-NL",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1723,6 +1736,7 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2066,6 +2080,7 @@ class AlexaToggleController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2212,6 +2227,7 @@ class AlexaPlaybackStateReporter(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2267,6 +2283,7 @@ class AlexaSeekController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2360,6 +2377,7 @@ class AlexaEqualizerController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2470,6 +2488,7 @@ class AlexaCameraStreamController(AlexaCapability):
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@ CONF_SUPPORTED_LOCALES = (
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -474,25 +474,30 @@ class ClimateCapabilities(AlexaEntity):
|
|||||||
# If we support two modes, one being off, we allow turning on too.
|
# If we support two modes, one being off, we allow turning on too.
|
||||||
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if (
|
if (
|
||||||
self.entity.domain == climate.DOMAIN
|
(
|
||||||
and climate.HVACMode.OFF
|
self.entity.domain == climate.DOMAIN
|
||||||
in (self.entity.attributes.get(climate.ATTR_HVAC_MODES) or [])
|
and climate.HVACMode.OFF
|
||||||
or self.entity.domain == climate.DOMAIN
|
in (self.entity.attributes.get(climate.ATTR_HVAC_MODES) or [])
|
||||||
and (
|
)
|
||||||
supported_features
|
or (
|
||||||
& (
|
self.entity.domain == climate.DOMAIN
|
||||||
climate.ClimateEntityFeature.TURN_ON
|
and (
|
||||||
| climate.ClimateEntityFeature.TURN_OFF
|
supported_features
|
||||||
|
& (
|
||||||
|
climate.ClimateEntityFeature.TURN_ON
|
||||||
|
| climate.ClimateEntityFeature.TURN_OFF
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
or self.entity.domain == water_heater.DOMAIN
|
or (
|
||||||
and (supported_features & water_heater.WaterHeaterEntityFeature.ON_OFF)
|
self.entity.domain == water_heater.DOMAIN
|
||||||
|
and (supported_features & water_heater.WaterHeaterEntityFeature.ON_OFF)
|
||||||
|
)
|
||||||
):
|
):
|
||||||
yield AlexaPowerController(self.entity)
|
yield AlexaPowerController(self.entity)
|
||||||
|
|
||||||
if (
|
if self.entity.domain == climate.DOMAIN or (
|
||||||
self.entity.domain == climate.DOMAIN
|
self.entity.domain == water_heater.DOMAIN
|
||||||
or self.entity.domain == water_heater.DOMAIN
|
|
||||||
and (
|
and (
|
||||||
supported_features
|
supported_features
|
||||||
& water_heater.WaterHeaterEntityFeature.OPERATION_MODE
|
& water_heater.WaterHeaterEntityFeature.OPERATION_MODE
|
||||||
|
@ -359,7 +359,7 @@ async def async_api_set_color_temperature(
|
|||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
entity.domain,
|
entity.domain,
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_KELVIN: kelvin},
|
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP_KELVIN: kelvin},
|
||||||
blocking=False,
|
blocking=False,
|
||||||
context=context,
|
context=context,
|
||||||
)
|
)
|
||||||
@ -376,14 +376,14 @@ async def async_api_decrease_color_temp(
|
|||||||
) -> AlexaResponse:
|
) -> AlexaResponse:
|
||||||
"""Process a decrease color temperature request."""
|
"""Process a decrease color temperature request."""
|
||||||
entity = directive.entity
|
entity = directive.entity
|
||||||
current = int(entity.attributes[light.ATTR_COLOR_TEMP])
|
current = int(entity.attributes[light.ATTR_COLOR_TEMP_KELVIN])
|
||||||
max_mireds = int(entity.attributes[light.ATTR_MAX_MIREDS])
|
min_kelvin = int(entity.attributes[light.ATTR_MIN_COLOR_TEMP_KELVIN])
|
||||||
|
|
||||||
value = min(max_mireds, current + 50)
|
value = max(min_kelvin, current - 500)
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
entity.domain,
|
entity.domain,
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP: value},
|
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP_KELVIN: value},
|
||||||
blocking=False,
|
blocking=False,
|
||||||
context=context,
|
context=context,
|
||||||
)
|
)
|
||||||
@ -400,14 +400,14 @@ async def async_api_increase_color_temp(
|
|||||||
) -> AlexaResponse:
|
) -> AlexaResponse:
|
||||||
"""Process an increase color temperature request."""
|
"""Process an increase color temperature request."""
|
||||||
entity = directive.entity
|
entity = directive.entity
|
||||||
current = int(entity.attributes[light.ATTR_COLOR_TEMP])
|
current = int(entity.attributes[light.ATTR_COLOR_TEMP_KELVIN])
|
||||||
min_mireds = int(entity.attributes[light.ATTR_MIN_MIREDS])
|
max_kelvin = int(entity.attributes[light.ATTR_MAX_COLOR_TEMP_KELVIN])
|
||||||
|
|
||||||
value = max(min_mireds, current - 50)
|
value = min(max_kelvin, current + 500)
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
entity.domain,
|
entity.domain,
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP: value},
|
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP_KELVIN: value},
|
||||||
blocking=False,
|
blocking=False,
|
||||||
context=context,
|
context=context,
|
||||||
)
|
)
|
||||||
@ -527,6 +527,7 @@ async def async_api_unlock(
|
|||||||
"hi-IN",
|
"hi-IN",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
"pt-BR",
|
"pt-BR",
|
||||||
}:
|
}:
|
||||||
msg = (
|
msg = (
|
||||||
|
@ -317,9 +317,8 @@ async def async_enable_proactive_mode(
|
|||||||
|
|
||||||
if should_doorbell:
|
if should_doorbell:
|
||||||
old_state = data["old_state"]
|
old_state = data["old_state"]
|
||||||
if (
|
if new_state.domain == event.DOMAIN or (
|
||||||
new_state.domain == event.DOMAIN
|
new_state.state == STATE_ON
|
||||||
or new_state.state == STATE_ON
|
|
||||||
and (old_state is None or old_state.state != STATE_ON)
|
and (old_state is None or old_state.state != STATE_ON)
|
||||||
):
|
):
|
||||||
await async_send_doorbell_event_message(
|
await async_send_doorbell_event_message(
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enable_motion_recording": {
|
"enable_motion_recording": {
|
||||||
"name": "Enables motion recording",
|
"name": "Enable motion recording",
|
||||||
"description": "Enables recording a clip to camera storage when motion is detected.",
|
"description": "Enables recording a clip to camera storage when motion is detected.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"entity_id": {
|
"entity_id": {
|
||||||
@ -51,8 +51,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"disable_motion_recording": {
|
"disable_motion_recording": {
|
||||||
"name": "Disables motion recording",
|
"name": "Disable motion recording",
|
||||||
"description": "Disable recording a clip to camera storage when motion is detected.",
|
"description": "Disables recording a clip to camera storage when motion is detected.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"entity_id": {
|
"entity_id": {
|
||||||
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
|
"name": "[%key:component::amcrest::services::enable_recording::fields::entity_id::name%]",
|
||||||
|
@ -11,12 +11,7 @@ from python_homeassistant_analytics import (
|
|||||||
from python_homeassistant_analytics.models import IntegrationType
|
from python_homeassistant_analytics.models import IntegrationType
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||||
ConfigEntry,
|
|
||||||
ConfigFlow,
|
|
||||||
ConfigFlowResult,
|
|
||||||
OptionsFlow,
|
|
||||||
)
|
|
||||||
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
|
||||||
from homeassistant.helpers.selector import (
|
from homeassistant.helpers.selector import (
|
||||||
@ -25,6 +20,7 @@ from homeassistant.helpers.selector import (
|
|||||||
SelectSelectorConfig,
|
SelectSelectorConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from . import AnalyticsInsightsConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_TRACKED_ADDONS,
|
CONF_TRACKED_ADDONS,
|
||||||
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
||||||
@ -46,7 +42,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
config_entry: ConfigEntry,
|
config_entry: AnalyticsInsightsConfigEntry,
|
||||||
) -> HomeassistantAnalyticsOptionsFlowHandler:
|
) -> HomeassistantAnalyticsOptionsFlowHandler:
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return HomeassistantAnalyticsOptionsFlowHandler()
|
return HomeassistantAnalyticsOptionsFlowHandler()
|
||||||
|
@ -7,6 +7,6 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["python_homeassistant_analytics"],
|
"loggers": ["python_homeassistant_analytics"],
|
||||||
"requirements": ["python-homeassistant-analytics==0.8.0"],
|
"requirements": ["python-homeassistant-analytics==0.8.1"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
100
homeassistant/components/analytics_insights/quality_scale.yaml
Normal file
100
homeassistant/components/analytics_insights/quality_scale.yaml
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration does not provide additional actions.
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration does not provide additional actions.
|
||||||
|
docs-high-level-description: todo
|
||||||
|
docs-installation-instructions: todo
|
||||||
|
docs-removal-instructions: todo
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Entities of this integration does not explicitly subscribe to events.
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure: done
|
||||||
|
test-before-setup: done
|
||||||
|
unique-config-entry: done
|
||||||
|
|
||||||
|
# Silver
|
||||||
|
action-exceptions:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration does not provide actions.
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters: todo
|
||||||
|
docs-installation-parameters: todo
|
||||||
|
entity-unavailable:
|
||||||
|
status: done
|
||||||
|
comment: |
|
||||||
|
The coordinator handles this.
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable:
|
||||||
|
status: done
|
||||||
|
comment: |
|
||||||
|
The coordinator handles this.
|
||||||
|
parallel-updates: todo
|
||||||
|
reauthentication-flow:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration does not require authentication.
|
||||||
|
test-coverage: todo
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration is a cloud service and thus does not support discovery.
|
||||||
|
discovery:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration is a cloud service and thus does not support discovery.
|
||||||
|
docs-data-update: todo
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: todo
|
||||||
|
docs-supported-devices: todo
|
||||||
|
docs-supported-functions: todo
|
||||||
|
docs-troubleshooting: todo
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration has a fixed single service.
|
||||||
|
entity-category: done
|
||||||
|
entity-device-class:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration does not have entities with device classes.
|
||||||
|
entity-disabled-by-default: done
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: todo
|
||||||
|
icon-translations: done
|
||||||
|
reconfiguration-flow:
|
||||||
|
status: exempt
|
||||||
|
comment: All the options of this integration are managed via the options flow
|
||||||
|
repair-issues:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration doesn't have any cases where raising an issue is needed.
|
||||||
|
stale-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration has a fixed single service.
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: done
|
@ -110,7 +110,7 @@ def _setup_androidtv(
|
|||||||
adb_log = f"using Python ADB implementation with adbkey='{adbkey}'"
|
adb_log = f"using Python ADB implementation with adbkey='{adbkey}'"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Use "pure-python-adb" (communicate with ADB server)
|
# Communicate via ADB server
|
||||||
signer = None
|
signer = None
|
||||||
adb_log = (
|
adb_log = (
|
||||||
"using ADB server at"
|
"using ADB server at"
|
||||||
@ -135,15 +135,16 @@ async def async_connect_androidtv(
|
|||||||
)
|
)
|
||||||
|
|
||||||
aftv = await async_androidtv_setup(
|
aftv = await async_androidtv_setup(
|
||||||
config[CONF_HOST],
|
host=config[CONF_HOST],
|
||||||
config[CONF_PORT],
|
port=config[CONF_PORT],
|
||||||
adbkey,
|
adbkey=adbkey,
|
||||||
config.get(CONF_ADB_SERVER_IP),
|
adb_server_ip=config.get(CONF_ADB_SERVER_IP),
|
||||||
config.get(CONF_ADB_SERVER_PORT, DEFAULT_ADB_SERVER_PORT),
|
adb_server_port=config.get(CONF_ADB_SERVER_PORT, DEFAULT_ADB_SERVER_PORT),
|
||||||
state_detection_rules,
|
state_detection_rules=state_detection_rules,
|
||||||
config[CONF_DEVICE_CLASS],
|
device_class=config[CONF_DEVICE_CLASS],
|
||||||
timeout,
|
auth_timeout_s=timeout,
|
||||||
signer,
|
signer=signer,
|
||||||
|
log_errors=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not aftv.available:
|
if not aftv.available:
|
||||||
|
@ -151,5 +151,5 @@ class AndroidTVEntity(Entity):
|
|||||||
# Using "adb_shell" (Python ADB implementation)
|
# Using "adb_shell" (Python ADB implementation)
|
||||||
self.exceptions = ADB_PYTHON_EXCEPTIONS
|
self.exceptions = ADB_PYTHON_EXCEPTIONS
|
||||||
else:
|
else:
|
||||||
# Using "pure-python-adb" (communicate with ADB server)
|
# Communicate via ADB server
|
||||||
self.exceptions = ADB_TCP_EXCEPTIONS
|
self.exceptions = ADB_TCP_EXCEPTIONS
|
||||||
|
@ -6,10 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["adb_shell", "androidtv", "pure_python_adb"],
|
"loggers": ["adb_shell", "androidtv"],
|
||||||
"requirements": [
|
"requirements": ["adb-shell[async]==0.4.4", "androidtv[async]==0.0.75"]
|
||||||
"adb-shell[async]==0.4.4",
|
|
||||||
"androidtv[async]==0.0.73",
|
|
||||||
"pure-python-adb[async]==0.3.0.dev0"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"invalid_unique_id": "Impossible to determine a valid unique id for the device"
|
"invalid_unique_id": "Impossible to determine a valid unique ID for the device"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@ -38,17 +38,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"title": "Configure Android Apps",
|
"title": "Configure Android apps",
|
||||||
"description": "Configure application id {app_id}",
|
"description": "Configure application ID {app_id}",
|
||||||
"data": {
|
"data": {
|
||||||
"app_name": "Application Name",
|
"app_name": "Application name",
|
||||||
"app_id": "Application ID",
|
"app_id": "Application ID",
|
||||||
"app_delete": "Check to delete this application"
|
"app_delete": "Check to delete this application"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"title": "Configure Android state detection rules",
|
"title": "Configure Android state detection rules",
|
||||||
"description": "Configure detection rule for application id {rule_id}",
|
"description": "Configure detection rule for application ID {rule_id}",
|
||||||
"data": {
|
"data": {
|
||||||
"rule_id": "[%key:component::androidtv::options::step::apps::data::app_id%]",
|
"rule_id": "[%key:component::androidtv::options::step::apps::data::app_id%]",
|
||||||
"rule_values": "List of state detection rules (see documentation)",
|
"rule_values": "List of state detection rules (see documentation)",
|
||||||
|
@ -14,7 +14,6 @@ from androidtvremote2 import (
|
|||||||
)
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import zeroconf
|
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
SOURCE_REAUTH,
|
SOURCE_REAUTH,
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
@ -31,6 +30,7 @@ from homeassistant.helpers.selector import (
|
|||||||
SelectSelectorConfig,
|
SelectSelectorConfig,
|
||||||
SelectSelectorMode,
|
SelectSelectorMode,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
|
||||||
from .const import CONF_APP_ICON, CONF_APP_NAME, CONF_APPS, CONF_ENABLE_IME, DOMAIN
|
from .const import CONF_APP_ICON, CONF_APP_NAME, CONF_APPS, CONF_ENABLE_IME, DOMAIN
|
||||||
from .helpers import create_api, get_enable_ime
|
from .helpers import create_api, get_enable_ime
|
||||||
@ -142,7 +142,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_zeroconf(
|
async def async_step_zeroconf(
|
||||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
self, discovery_info: ZeroconfServiceInfo
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle zeroconf discovery."""
|
"""Handle zeroconf discovery."""
|
||||||
_LOGGER.debug("Android TV device found via zeroconf: %s", discovery_info)
|
_LOGGER.debug("Android TV device found via zeroconf: %s", discovery_info)
|
||||||
@ -156,7 +156,12 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
# and one of them, which could end up being in discovery_info.host, is from a
|
# and one of them, which could end up being in discovery_info.host, is from a
|
||||||
# different device. If any of the discovery_info.ip_addresses matches the
|
# different device. If any of the discovery_info.ip_addresses matches the
|
||||||
# existing host, don't update the host.
|
# existing host, don't update the host.
|
||||||
if existing_config_entry and len(discovery_info.ip_addresses) > 1:
|
if (
|
||||||
|
existing_config_entry
|
||||||
|
# Ignored entries don't have host
|
||||||
|
and CONF_HOST in existing_config_entry.data
|
||||||
|
and len(discovery_info.ip_addresses) > 1
|
||||||
|
):
|
||||||
existing_host = existing_config_entry.data[CONF_HOST]
|
existing_host = existing_config_entry.data[CONF_HOST]
|
||||||
if existing_host != self.host:
|
if existing_host != self.host:
|
||||||
if existing_host in [
|
if existing_host in [
|
||||||
|
@ -44,12 +44,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"title": "Configure Android Apps",
|
"title": "Configure Android apps",
|
||||||
"description": "Configure application id {app_id}",
|
"description": "Configure application ID {app_id}",
|
||||||
"data": {
|
"data": {
|
||||||
"app_name": "Application Name",
|
"app_name": "Application name",
|
||||||
"app_id": "Application ID",
|
"app_id": "Application ID",
|
||||||
"app_icon": "Application Icon",
|
"app_icon": "Application icon",
|
||||||
"app_delete": "Check to delete this application"
|
"app_delete": "Check to delete this application"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import HomeAssistantError, TemplateError
|
from homeassistant.exceptions import HomeAssistantError, TemplateError
|
||||||
from homeassistant.helpers import device_registry as dr, intent, llm, template
|
from homeassistant.helpers import device_registry as dr, intent, llm, template
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import ulid
|
from homeassistant.util import ulid as ulid_util
|
||||||
|
|
||||||
from . import AnthropicConfigEntry
|
from . import AnthropicConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -164,7 +164,7 @@ class AnthropicConversationEntity(
|
|||||||
]
|
]
|
||||||
|
|
||||||
if user_input.conversation_id is None:
|
if user_input.conversation_id is None:
|
||||||
conversation_id = ulid.ulid_now()
|
conversation_id = ulid_util.ulid_now()
|
||||||
messages = []
|
messages = []
|
||||||
|
|
||||||
elif user_input.conversation_id in self.history:
|
elif user_input.conversation_id in self.history:
|
||||||
@ -177,8 +177,8 @@ class AnthropicConversationEntity(
|
|||||||
# a new conversation was started. If the user picks their own, they
|
# a new conversation was started. If the user picks their own, they
|
||||||
# want to track a conversation and we respect it.
|
# want to track a conversation and we respect it.
|
||||||
try:
|
try:
|
||||||
ulid.ulid_to_bytes(user_input.conversation_id)
|
ulid_util.ulid_to_bytes(user_input.conversation_id)
|
||||||
conversation_id = ulid.ulid_now()
|
conversation_id = ulid_util.ulid_now()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
conversation_id = user_input.conversation_id
|
conversation_id = user_input.conversation_id
|
||||||
|
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["anthropic==0.31.2"]
|
"requirements": ["anthropic==0.44.0"]
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["py-aosmith==1.0.11"]
|
"requirements": ["py-aosmith==1.0.12"]
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user