mirror of
https://github.com/home-assistant/core.git
synced 2025-07-29 16:17:20 +00:00
Merge branch 'dev' into ingress_dropping_close
This commit is contained in:
commit
b2a737cf56
@ -127,6 +127,7 @@ tests: &tests
|
|||||||
- tests/*.py
|
- tests/*.py
|
||||||
- tests/auth/**
|
- tests/auth/**
|
||||||
- tests/backports/**
|
- tests/backports/**
|
||||||
|
- tests/components/conftest.py
|
||||||
- tests/components/diagnostics/**
|
- tests/components/diagnostics/**
|
||||||
- tests/components/history/**
|
- tests/components/history/**
|
||||||
- tests/components/logbook/**
|
- tests/components/logbook/**
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "Home Assistant Dev",
|
"name": "Home Assistant Dev",
|
||||||
"context": "..",
|
"context": "..",
|
||||||
"dockerFile": "../Dockerfile.dev",
|
"dockerFile": "../Dockerfile.dev",
|
||||||
"postCreateCommand": "script/setup",
|
"postCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && script/setup",
|
||||||
"postStartCommand": "script/bootstrap",
|
"postStartCommand": "script/bootstrap",
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
"PYTHONASYNCIODEBUG": "1"
|
"PYTHONASYNCIODEBUG": "1"
|
||||||
@ -12,7 +12,12 @@
|
|||||||
},
|
},
|
||||||
// Port 5683 udp is used by Shelly integration
|
// Port 5683 udp is used by Shelly integration
|
||||||
"appPort": ["8123:8123", "5683:5683/udp"],
|
"appPort": ["8123:8123", "5683:5683/udp"],
|
||||||
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
|
"runArgs": [
|
||||||
|
"-e",
|
||||||
|
"GIT_EDITOR=code --wait",
|
||||||
|
"--security-opt",
|
||||||
|
"label=disable"
|
||||||
|
],
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
@ -53,7 +58,13 @@
|
|||||||
],
|
],
|
||||||
"[python]": {
|
"[python]": {
|
||||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||||
}
|
},
|
||||||
|
"json.schemas": [
|
||||||
|
{
|
||||||
|
"fileMatch": ["homeassistant/components/*/manifest.json"],
|
||||||
|
"url": "./script/json_schemas/manifest_schema.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ docs
|
|||||||
# Development
|
# Development
|
||||||
.devcontainer
|
.devcontainer
|
||||||
.vscode
|
.vscode
|
||||||
|
.tool-versions
|
||||||
|
|
||||||
# Test related files
|
# Test related files
|
||||||
tests
|
tests
|
||||||
|
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@ -1,2 +1 @@
|
|||||||
custom: https://www.nabucasa.com
|
custom: https://www.openhomefoundation.org
|
||||||
github: balloob
|
|
||||||
|
28
.github/workflows/builder.yml
vendored
28
.github/workflows/builder.yml
vendored
@ -27,12 +27,12 @@ jobs:
|
|||||||
publish: ${{ steps.version.outputs.publish }}
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ jobs:
|
|||||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
@ -90,7 +90,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
@ -116,7 +116,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@ -242,7 +242,7 @@ jobs:
|
|||||||
- green
|
- green
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
@ -279,7 +279,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
uses: home-assistant/actions/helpers/git-init@master
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
@ -321,10 +321,10 @@ jobs:
|
|||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.6.0
|
uses: sigstore/cosign-installer@v3.7.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.2.3"
|
cosign-release: "v2.2.3"
|
||||||
|
|
||||||
@ -451,10 +451,10 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@ -499,7 +499,7 @@ jobs:
|
|||||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
@ -509,7 +509,7 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
|
||||||
with:
|
with:
|
||||||
context: . # So action will not pull the repository again
|
context: . # So action will not pull the repository again
|
||||||
file: ./script/hassfest/docker/Dockerfile
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
@ -522,7 +522,7 @@ jobs:
|
|||||||
- name: Push Docker image
|
- name: Push Docker image
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
|
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
|
||||||
with:
|
with:
|
||||||
context: . # So action will not pull the repository again
|
context: . # So action will not pull the repository again
|
||||||
file: ./script/hassfest/docker/Dockerfile
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
|
188
.github/workflows/ci.yaml
vendored
188
.github/workflows/ci.yaml
vendored
@ -37,10 +37,10 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CACHE_VERSION: 10
|
CACHE_VERSION: 11
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 9
|
MYPY_CACHE_VERSION: 9
|
||||||
HA_SHORT_VERSION: "2024.10"
|
HA_SHORT_VERSION: "2024.12"
|
||||||
DEFAULT_PYTHON: "3.12"
|
DEFAULT_PYTHON: "3.12"
|
||||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
@ -93,7 +93,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Generate partial Python venv restore key
|
- name: Generate partial Python venv restore key
|
||||||
id: generate_python_cache_key
|
id: generate_python_cache_key
|
||||||
run: |
|
run: |
|
||||||
@ -231,16 +231,16 @@ jobs:
|
|||||||
- info
|
- info
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.0.2
|
uses: actions/cache@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: >-
|
key: >-
|
||||||
@ -256,7 +256,7 @@ jobs:
|
|||||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v4.0.2
|
uses: actions/cache@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
@ -277,16 +277,16 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -295,7 +295,7 @@ jobs:
|
|||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -317,16 +317,16 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -335,7 +335,7 @@ jobs:
|
|||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -357,16 +357,16 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -375,7 +375,7 @@ jobs:
|
|||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -447,7 +447,7 @@ jobs:
|
|||||||
- script/hassfest/docker/Dockerfile
|
- script/hassfest/docker/Dockerfile
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Register hadolint problem matcher
|
- name: Register hadolint problem matcher
|
||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||||
@ -466,10 +466,10 @@ jobs:
|
|||||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -482,7 +482,7 @@ jobs:
|
|||||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.0.2
|
uses: actions/cache@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
@ -491,7 +491,7 @@ jobs:
|
|||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Restore uv wheel cache
|
- name: Restore uv wheel cache
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache@v4.0.2
|
uses: actions/cache@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.UV_CACHE_DIR }}
|
path: ${{ env.UV_CACHE_DIR }}
|
||||||
key: >-
|
key: >-
|
||||||
@ -550,16 +550,16 @@ jobs:
|
|||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
libturbojpeg
|
libturbojpeg
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -583,16 +583,16 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -615,37 +615,41 @@ jobs:
|
|||||||
&& github.event.inputs.mypy-only != 'true'
|
&& github.event.inputs.mypy-only != 'true'
|
||||||
|| github.event.inputs.audit-licenses-only == 'true')
|
|| github.event.inputs.audit-licenses-only == 'true')
|
||||||
&& needs.info.outputs.requirements == 'true'
|
&& needs.info.outputs.requirements == 'true'
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
key: >-
|
key: >-
|
||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Run pip-licenses
|
- name: Extract license data
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pip-licenses --format=json --output-file=licenses.json
|
python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json
|
||||||
- name: Upload licenses
|
- name: Upload licenses
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: licenses
|
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
||||||
path: licenses.json
|
path: licenses-${{ matrix.python-version }}.json
|
||||||
- name: Process licenses
|
- name: Check licenses
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python -m script.licenses
|
python -m script.licenses check licenses-${{ matrix.python-version }}.json
|
||||||
|
|
||||||
pylint:
|
pylint:
|
||||||
name: Check pylint
|
name: Check pylint
|
||||||
@ -660,16 +664,16 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -707,16 +711,16 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -752,10 +756,10 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -768,7 +772,7 @@ jobs:
|
|||||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -776,7 +780,7 @@ jobs:
|
|||||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Restore mypy cache
|
- name: Restore mypy cache
|
||||||
uses: actions/cache@v4.0.2
|
uses: actions/cache@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: .mypy_cache
|
path: .mypy_cache
|
||||||
key: >-
|
key: >-
|
||||||
@ -815,7 +819,11 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
- base
|
- base
|
||||||
name: Split tests for full run
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||||
|
name: Split tests for full run Python ${{ matrix.python-version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install additional OS dependencies
|
- name: Install additional OS dependencies
|
||||||
run: |
|
run: |
|
||||||
@ -827,16 +835,16 @@ jobs:
|
|||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -848,9 +856,9 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
||||||
- name: Upload pytest_buckets
|
- name: Upload pytest_buckets
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets-${{ matrix.python-version }}
|
||||||
path: pytest_buckets.txt
|
path: pytest_buckets.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
@ -891,16 +899,16 @@ jobs:
|
|||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -915,7 +923,7 @@ jobs:
|
|||||||
- name: Download pytest_buckets
|
- name: Download pytest_buckets
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets-${{ matrix.python-version }}
|
||||||
- name: Compile English translations
|
- name: Compile English translations
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
@ -940,7 +948,7 @@ jobs:
|
|||||||
-qq \
|
-qq \
|
||||||
--timeout=9 \
|
--timeout=9 \
|
||||||
--durations=10 \
|
--durations=10 \
|
||||||
-n auto \
|
--numprocesses auto \
|
||||||
--dist=loadfile \
|
--dist=loadfile \
|
||||||
${cov_params[@]} \
|
${cov_params[@]} \
|
||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
@ -949,14 +957,14 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: pytest-*.txt
|
path: pytest-*.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
@ -1011,16 +1019,16 @@ jobs:
|
|||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libmariadb-dev-compat
|
libmariadb-dev-compat
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -1062,7 +1070,7 @@ jobs:
|
|||||||
python3 -b -X dev -m pytest \
|
python3 -b -X dev -m pytest \
|
||||||
-qq \
|
-qq \
|
||||||
--timeout=20 \
|
--timeout=20 \
|
||||||
-n 1 \
|
--numprocesses 1 \
|
||||||
${cov_params[@]} \
|
${cov_params[@]} \
|
||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
--durations=10 \
|
--durations=10 \
|
||||||
@ -1075,7 +1083,7 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.mariadb }}
|
steps.pytest-partial.outputs.mariadb }}
|
||||||
@ -1083,7 +1091,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{
|
name: coverage-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.mariadb }}
|
steps.pytest-partial.outputs.mariadb }}
|
||||||
@ -1094,7 +1102,7 @@ jobs:
|
|||||||
./script/check_dirty
|
./script/check_dirty
|
||||||
|
|
||||||
pytest-postgres:
|
pytest-postgres:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: ${{ matrix.postgresql-group }}
|
image: ${{ matrix.postgresql-group }}
|
||||||
@ -1134,19 +1142,21 @@ jobs:
|
|||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
bluez \
|
bluez \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
libturbojpeg \
|
libturbojpeg
|
||||||
|
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
|
||||||
|
sudo apt-get -y install \
|
||||||
postgresql-server-dev-14
|
postgresql-server-dev-14
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -1188,7 +1198,7 @@ jobs:
|
|||||||
python3 -b -X dev -m pytest \
|
python3 -b -X dev -m pytest \
|
||||||
-qq \
|
-qq \
|
||||||
--timeout=9 \
|
--timeout=9 \
|
||||||
-n 1 \
|
--numprocesses 1 \
|
||||||
${cov_params[@]} \
|
${cov_params[@]} \
|
||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
--durations=0 \
|
--durations=0 \
|
||||||
@ -1202,7 +1212,7 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.postgresql }}
|
steps.pytest-partial.outputs.postgresql }}
|
||||||
@ -1210,7 +1220,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{
|
name: coverage-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.postgresql }}
|
steps.pytest-partial.outputs.postgresql }}
|
||||||
@ -1232,14 +1242,14 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'true'
|
if: needs.info.outputs.test_full_suite == 'true'
|
||||||
uses: codecov/codecov-action@v4.5.0
|
uses: codecov/codecov-action@v4.6.0
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
flags: full-suite
|
flags: full-suite
|
||||||
@ -1283,16 +1293,16 @@ jobs:
|
|||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@v4.0.2
|
uses: actions/cache/restore@v4.1.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@ -1334,7 +1344,7 @@ jobs:
|
|||||||
python3 -b -X dev -m pytest \
|
python3 -b -X dev -m pytest \
|
||||||
-qq \
|
-qq \
|
||||||
--timeout=9 \
|
--timeout=9 \
|
||||||
-n auto \
|
--numprocesses auto \
|
||||||
${cov_params[@]} \
|
${cov_params[@]} \
|
||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
--durations=0 \
|
--durations=0 \
|
||||||
@ -1344,14 +1354,14 @@ jobs:
|
|||||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: pytest-*.txt
|
path: pytest-*.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
@ -1370,14 +1380,14 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'false'
|
if: needs.info.outputs.test_full_suite == 'false'
|
||||||
uses: codecov/codecov-action@v4.5.0
|
uses: codecov/codecov-action@v4.6.0
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -21,14 +21,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.26.9
|
uses: github/codeql-action/init@v3.27.0
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.26.9
|
uses: github/codeql-action/analyze@v3.27.0
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@ -19,10 +19,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
27
.github/workflows/wheels.yml
vendored
27
.github/workflows/wheels.yml
vendored
@ -32,11 +32,11 @@ jobs:
|
|||||||
architectures: ${{ steps.info.outputs.architectures }}
|
architectures: ${{ steps.info.outputs.architectures }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.2.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@ -64,11 +64,8 @@ jobs:
|
|||||||
- name: Write env-file
|
- name: Write env-file
|
||||||
run: |
|
run: |
|
||||||
(
|
(
|
||||||
echo "GRPC_BUILD_WITH_BORING_SSL_ASM=false"
|
|
||||||
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true"
|
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true"
|
||||||
echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true"
|
echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true"
|
||||||
echo "GRPC_PYTHON_DISABLE_LIBC_COMPATIBILITY=true"
|
|
||||||
echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc"
|
|
||||||
|
|
||||||
# Fix out of memory issues with rust
|
# Fix out of memory issues with rust
|
||||||
echo "CARGO_NET_GIT_FETCH_WITH_CLI=true"
|
echo "CARGO_NET_GIT_FETCH_WITH_CLI=true"
|
||||||
@ -82,7 +79,7 @@ jobs:
|
|||||||
) > .env_file
|
) > .env_file
|
||||||
|
|
||||||
- name: Upload env_file
|
- name: Upload env_file
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
path: ./.env_file
|
path: ./.env_file
|
||||||
@ -90,7 +87,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
- name: Upload requirements_diff
|
- name: Upload requirements_diff
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
path: ./requirements_diff.txt
|
path: ./requirements_diff.txt
|
||||||
@ -102,7 +99,7 @@ jobs:
|
|||||||
python -m script.gen_requirements_all ci
|
python -m script.gen_requirements_all ci
|
||||||
|
|
||||||
- name: Upload requirements_all_wheels
|
- name: Upload requirements_all_wheels
|
||||||
uses: actions/upload-artifact@v4.4.0
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
path: ./requirements_all_wheels_*.txt
|
path: ./requirements_all_wheels_*.txt
|
||||||
@ -119,7 +116,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
@ -163,7 +160,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.7
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.8
|
||||||
@ -205,11 +202,9 @@ jobs:
|
|||||||
# Some dependencies still require 'cython<3'
|
# Some dependencies still require 'cython<3'
|
||||||
# and don't yet use isolated build environments.
|
# and don't yet use isolated build environments.
|
||||||
# Build these first.
|
# Build these first.
|
||||||
# grpcio: https://github.com/grpc/grpc/issues/33918
|
|
||||||
# pydantic: https://github.com/pydantic/pydantic/issues/7689
|
# pydantic: https://github.com/pydantic/pydantic/issues/7689
|
||||||
|
|
||||||
touch requirements_old-cython.txt
|
touch requirements_old-cython.txt
|
||||||
cat homeassistant/package_constraints.txt | grep 'grpcio==' >> requirements_old-cython.txt
|
|
||||||
cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt
|
cat homeassistant/package_constraints.txt | grep 'pydantic==' >> requirements_old-cython.txt
|
||||||
|
|
||||||
- name: Build wheels (old cython)
|
- name: Build wheels (old cython)
|
||||||
@ -221,7 +216,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_old-cython.txt"
|
requirements: "requirements_old-cython.txt"
|
||||||
@ -236,7 +231,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtaa"
|
requirements: "requirements_all.txtaa"
|
||||||
@ -250,7 +245,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtab"
|
requirements: "requirements_all.txtab"
|
||||||
@ -264,7 +259,7 @@ jobs:
|
|||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;protobuf;pydantic;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pydantic;pymicro-vad;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txtac"
|
requirements: "requirements_all.txtac"
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -79,6 +79,7 @@ pytest-*.txt
|
|||||||
.pydevproject
|
.pydevproject
|
||||||
|
|
||||||
.python-version
|
.python-version
|
||||||
|
.tool-versions
|
||||||
|
|
||||||
# emacs auto backups
|
# emacs auto backups
|
||||||
*~
|
*~
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.6.6
|
rev: v0.7.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
@ -83,10 +83,10 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
types: [text]
|
||||||
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements\.txt)$
|
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
|
||||||
- id: hassfest-metadata
|
- id: hassfest-metadata
|
||||||
name: hassfest-metadata
|
name: hassfest-metadata
|
||||||
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
|
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata,docker
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
types: [text]
|
||||||
|
@ -124,6 +124,7 @@ homeassistant.components.bryant_evolution.*
|
|||||||
homeassistant.components.bthome.*
|
homeassistant.components.bthome.*
|
||||||
homeassistant.components.button.*
|
homeassistant.components.button.*
|
||||||
homeassistant.components.calendar.*
|
homeassistant.components.calendar.*
|
||||||
|
homeassistant.components.cambridge_audio.*
|
||||||
homeassistant.components.camera.*
|
homeassistant.components.camera.*
|
||||||
homeassistant.components.canary.*
|
homeassistant.components.canary.*
|
||||||
homeassistant.components.cert_expiry.*
|
homeassistant.components.cert_expiry.*
|
||||||
@ -208,12 +209,14 @@ homeassistant.components.geo_location.*
|
|||||||
homeassistant.components.geocaching.*
|
homeassistant.components.geocaching.*
|
||||||
homeassistant.components.gios.*
|
homeassistant.components.gios.*
|
||||||
homeassistant.components.glances.*
|
homeassistant.components.glances.*
|
||||||
|
homeassistant.components.go2rtc.*
|
||||||
homeassistant.components.goalzero.*
|
homeassistant.components.goalzero.*
|
||||||
homeassistant.components.google.*
|
homeassistant.components.google.*
|
||||||
homeassistant.components.google_assistant_sdk.*
|
homeassistant.components.google_assistant_sdk.*
|
||||||
homeassistant.components.google_cloud.*
|
homeassistant.components.google_cloud.*
|
||||||
homeassistant.components.google_photos.*
|
homeassistant.components.google_photos.*
|
||||||
homeassistant.components.google_sheets.*
|
homeassistant.components.google_sheets.*
|
||||||
|
homeassistant.components.govee_ble.*
|
||||||
homeassistant.components.gpsd.*
|
homeassistant.components.gpsd.*
|
||||||
homeassistant.components.greeneye_monitor.*
|
homeassistant.components.greeneye_monitor.*
|
||||||
homeassistant.components.group.*
|
homeassistant.components.group.*
|
||||||
@ -301,7 +304,6 @@ homeassistant.components.lookin.*
|
|||||||
homeassistant.components.luftdaten.*
|
homeassistant.components.luftdaten.*
|
||||||
homeassistant.components.madvr.*
|
homeassistant.components.madvr.*
|
||||||
homeassistant.components.manual.*
|
homeassistant.components.manual.*
|
||||||
homeassistant.components.map.*
|
|
||||||
homeassistant.components.mastodon.*
|
homeassistant.components.mastodon.*
|
||||||
homeassistant.components.matrix.*
|
homeassistant.components.matrix.*
|
||||||
homeassistant.components.matter.*
|
homeassistant.components.matter.*
|
||||||
@ -322,6 +324,7 @@ homeassistant.components.moon.*
|
|||||||
homeassistant.components.mopeka.*
|
homeassistant.components.mopeka.*
|
||||||
homeassistant.components.motionmount.*
|
homeassistant.components.motionmount.*
|
||||||
homeassistant.components.mqtt.*
|
homeassistant.components.mqtt.*
|
||||||
|
homeassistant.components.music_assistant.*
|
||||||
homeassistant.components.my.*
|
homeassistant.components.my.*
|
||||||
homeassistant.components.mysensors.*
|
homeassistant.components.mysensors.*
|
||||||
homeassistant.components.myuplink.*
|
homeassistant.components.myuplink.*
|
||||||
@ -345,6 +348,7 @@ homeassistant.components.oncue.*
|
|||||||
homeassistant.components.onewire.*
|
homeassistant.components.onewire.*
|
||||||
homeassistant.components.onkyo.*
|
homeassistant.components.onkyo.*
|
||||||
homeassistant.components.open_meteo.*
|
homeassistant.components.open_meteo.*
|
||||||
|
homeassistant.components.openai_conversation.*
|
||||||
homeassistant.components.openexchangerates.*
|
homeassistant.components.openexchangerates.*
|
||||||
homeassistant.components.opensky.*
|
homeassistant.components.opensky.*
|
||||||
homeassistant.components.openuv.*
|
homeassistant.components.openuv.*
|
||||||
@ -352,6 +356,7 @@ homeassistant.components.oralb.*
|
|||||||
homeassistant.components.otbr.*
|
homeassistant.components.otbr.*
|
||||||
homeassistant.components.overkiz.*
|
homeassistant.components.overkiz.*
|
||||||
homeassistant.components.p1_monitor.*
|
homeassistant.components.p1_monitor.*
|
||||||
|
homeassistant.components.panel_custom.*
|
||||||
homeassistant.components.peco.*
|
homeassistant.components.peco.*
|
||||||
homeassistant.components.persistent_notification.*
|
homeassistant.components.persistent_notification.*
|
||||||
homeassistant.components.pi_hole.*
|
homeassistant.components.pi_hole.*
|
||||||
@ -369,6 +374,7 @@ homeassistant.components.pvoutput.*
|
|||||||
homeassistant.components.qnap_qsw.*
|
homeassistant.components.qnap_qsw.*
|
||||||
homeassistant.components.rabbitair.*
|
homeassistant.components.rabbitair.*
|
||||||
homeassistant.components.radarr.*
|
homeassistant.components.radarr.*
|
||||||
|
homeassistant.components.radio_browser.*
|
||||||
homeassistant.components.rainforest_raven.*
|
homeassistant.components.rainforest_raven.*
|
||||||
homeassistant.components.rainmachine.*
|
homeassistant.components.rainmachine.*
|
||||||
homeassistant.components.raspberry_pi.*
|
homeassistant.components.raspberry_pi.*
|
||||||
@ -406,6 +412,7 @@ homeassistant.components.sensor.*
|
|||||||
homeassistant.components.sensoterra.*
|
homeassistant.components.sensoterra.*
|
||||||
homeassistant.components.senz.*
|
homeassistant.components.senz.*
|
||||||
homeassistant.components.sfr_box.*
|
homeassistant.components.sfr_box.*
|
||||||
|
homeassistant.components.shell_command.*
|
||||||
homeassistant.components.shelly.*
|
homeassistant.components.shelly.*
|
||||||
homeassistant.components.shopping_list.*
|
homeassistant.components.shopping_list.*
|
||||||
homeassistant.components.simplepush.*
|
homeassistant.components.simplepush.*
|
||||||
@ -420,6 +427,7 @@ homeassistant.components.snooz.*
|
|||||||
homeassistant.components.solarlog.*
|
homeassistant.components.solarlog.*
|
||||||
homeassistant.components.sonarr.*
|
homeassistant.components.sonarr.*
|
||||||
homeassistant.components.speedtestdotnet.*
|
homeassistant.components.speedtestdotnet.*
|
||||||
|
homeassistant.components.spotify.*
|
||||||
homeassistant.components.sql.*
|
homeassistant.components.sql.*
|
||||||
homeassistant.components.squeezebox.*
|
homeassistant.components.squeezebox.*
|
||||||
homeassistant.components.ssdp.*
|
homeassistant.components.ssdp.*
|
||||||
@ -434,6 +442,7 @@ homeassistant.components.suez_water.*
|
|||||||
homeassistant.components.sun.*
|
homeassistant.components.sun.*
|
||||||
homeassistant.components.surepetcare.*
|
homeassistant.components.surepetcare.*
|
||||||
homeassistant.components.switch.*
|
homeassistant.components.switch.*
|
||||||
|
homeassistant.components.switch_as_x.*
|
||||||
homeassistant.components.switchbee.*
|
homeassistant.components.switchbee.*
|
||||||
homeassistant.components.switchbot_cloud.*
|
homeassistant.components.switchbot_cloud.*
|
||||||
homeassistant.components.switcher_kis.*
|
homeassistant.components.switcher_kis.*
|
||||||
@ -502,6 +511,7 @@ homeassistant.components.whois.*
|
|||||||
homeassistant.components.withings.*
|
homeassistant.components.withings.*
|
||||||
homeassistant.components.wiz.*
|
homeassistant.components.wiz.*
|
||||||
homeassistant.components.wled.*
|
homeassistant.components.wled.*
|
||||||
|
homeassistant.components.workday.*
|
||||||
homeassistant.components.worldclock.*
|
homeassistant.components.worldclock.*
|
||||||
homeassistant.components.xiaomi_ble.*
|
homeassistant.components.xiaomi_ble.*
|
||||||
homeassistant.components.yale_smart_alarm.*
|
homeassistant.components.yale_smart_alarm.*
|
||||||
|
10
.vscode/settings.default.json
vendored
10
.vscode/settings.default.json
vendored
@ -6,5 +6,13 @@
|
|||||||
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
|
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
|
||||||
"python.testing.pytestEnabled": false,
|
"python.testing.pytestEnabled": false,
|
||||||
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
||||||
"pylint.importStrategy": "fromEnvironment"
|
"pylint.importStrategy": "fromEnvironment",
|
||||||
|
"json.schemas": [
|
||||||
|
{
|
||||||
|
"fileMatch": [
|
||||||
|
"homeassistant/components/*/manifest.json"
|
||||||
|
],
|
||||||
|
"url": "./script/json_schemas/manifest_schema.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
32
CODEOWNERS
32
CODEOWNERS
@ -544,6 +544,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/github/ @timmo001 @ludeeus
|
/tests/components/github/ @timmo001 @ludeeus
|
||||||
/homeassistant/components/glances/ @engrbm87
|
/homeassistant/components/glances/ @engrbm87
|
||||||
/tests/components/glances/ @engrbm87
|
/tests/components/glances/ @engrbm87
|
||||||
|
/homeassistant/components/go2rtc/ @home-assistant/core
|
||||||
|
/tests/components/go2rtc/ @home-assistant/core
|
||||||
/homeassistant/components/goalzero/ @tkdrob
|
/homeassistant/components/goalzero/ @tkdrob
|
||||||
/tests/components/goalzero/ @tkdrob
|
/tests/components/goalzero/ @tkdrob
|
||||||
/homeassistant/components/gogogate2/ @vangorra
|
/homeassistant/components/gogogate2/ @vangorra
|
||||||
@ -615,8 +617,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/hlk_sw16/ @jameshilliard
|
/tests/components/hlk_sw16/ @jameshilliard
|
||||||
/homeassistant/components/holiday/ @jrieger @gjohansson-ST
|
/homeassistant/components/holiday/ @jrieger @gjohansson-ST
|
||||||
/tests/components/holiday/ @jrieger @gjohansson-ST
|
/tests/components/holiday/ @jrieger @gjohansson-ST
|
||||||
/homeassistant/components/home_connect/ @DavidMStraub
|
/homeassistant/components/home_connect/ @DavidMStraub @Diegorro98
|
||||||
/tests/components/home_connect/ @DavidMStraub
|
/tests/components/home_connect/ @DavidMStraub @Diegorro98
|
||||||
/homeassistant/components/homeassistant/ @home-assistant/core
|
/homeassistant/components/homeassistant/ @home-assistant/core
|
||||||
/tests/components/homeassistant/ @home-assistant/core
|
/tests/components/homeassistant/ @home-assistant/core
|
||||||
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
|
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
|
||||||
@ -657,6 +659,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||||
/homeassistant/components/husqvarna_automower/ @Thomas55555
|
/homeassistant/components/husqvarna_automower/ @Thomas55555
|
||||||
/tests/components/husqvarna_automower/ @Thomas55555
|
/tests/components/husqvarna_automower/ @Thomas55555
|
||||||
|
/homeassistant/components/husqvarna_automower_ble/ @alistair23
|
||||||
|
/tests/components/husqvarna_automower_ble/ @alistair23
|
||||||
/homeassistant/components/huum/ @frwickst
|
/homeassistant/components/huum/ @frwickst
|
||||||
/tests/components/huum/ @frwickst
|
/tests/components/huum/ @frwickst
|
||||||
/homeassistant/components/hvv_departures/ @vigonotion
|
/homeassistant/components/hvv_departures/ @vigonotion
|
||||||
@ -817,6 +821,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/lektrico/ @lektrico
|
/tests/components/lektrico/ @lektrico
|
||||||
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
||||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||||
|
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
|
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
/homeassistant/components/lidarr/ @tkdrob
|
/homeassistant/components/lidarr/ @tkdrob
|
||||||
/tests/components/lidarr/ @tkdrob
|
/tests/components/lidarr/ @tkdrob
|
||||||
/homeassistant/components/lifx/ @Djelibeybi
|
/homeassistant/components/lifx/ @Djelibeybi
|
||||||
@ -948,6 +954,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/msteams/ @peroyvind
|
/homeassistant/components/msteams/ @peroyvind
|
||||||
/homeassistant/components/mullvad/ @meichthys
|
/homeassistant/components/mullvad/ @meichthys
|
||||||
/tests/components/mullvad/ @meichthys
|
/tests/components/mullvad/ @meichthys
|
||||||
|
/homeassistant/components/music_assistant/ @music-assistant
|
||||||
|
/tests/components/music_assistant/ @music-assistant
|
||||||
/homeassistant/components/mutesync/ @currentoor
|
/homeassistant/components/mutesync/ @currentoor
|
||||||
/tests/components/mutesync/ @currentoor
|
/tests/components/mutesync/ @currentoor
|
||||||
/homeassistant/components/my/ @home-assistant/core
|
/homeassistant/components/my/ @home-assistant/core
|
||||||
@ -1045,6 +1053,7 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/onewire/ @garbled1 @epenet
|
/homeassistant/components/onewire/ @garbled1 @epenet
|
||||||
/tests/components/onewire/ @garbled1 @epenet
|
/tests/components/onewire/ @garbled1 @epenet
|
||||||
/homeassistant/components/onkyo/ @arturpragacz
|
/homeassistant/components/onkyo/ @arturpragacz
|
||||||
|
/tests/components/onkyo/ @arturpragacz
|
||||||
/homeassistant/components/onvif/ @hunterjm
|
/homeassistant/components/onvif/ @hunterjm
|
||||||
/tests/components/onvif/ @hunterjm
|
/tests/components/onvif/ @hunterjm
|
||||||
/homeassistant/components/open_meteo/ @frenck
|
/homeassistant/components/open_meteo/ @frenck
|
||||||
@ -1086,10 +1095,10 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ovo_energy/ @timmo001
|
/tests/components/ovo_energy/ @timmo001
|
||||||
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
||||||
/tests/components/p1_monitor/ @klaasnicolaas
|
/tests/components/p1_monitor/ @klaasnicolaas
|
||||||
|
/homeassistant/components/palazzetti/ @dotvav
|
||||||
|
/tests/components/palazzetti/ @dotvav
|
||||||
/homeassistant/components/panel_custom/ @home-assistant/frontend
|
/homeassistant/components/panel_custom/ @home-assistant/frontend
|
||||||
/tests/components/panel_custom/ @home-assistant/frontend
|
/tests/components/panel_custom/ @home-assistant/frontend
|
||||||
/homeassistant/components/panel_iframe/ @home-assistant/frontend
|
|
||||||
/tests/components/panel_iframe/ @home-assistant/frontend
|
|
||||||
/homeassistant/components/peco/ @IceBotYT
|
/homeassistant/components/peco/ @IceBotYT
|
||||||
/tests/components/peco/ @IceBotYT
|
/tests/components/peco/ @IceBotYT
|
||||||
/homeassistant/components/pegel_online/ @mib1185
|
/homeassistant/components/pegel_online/ @mib1185
|
||||||
@ -1237,8 +1246,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/roku/ @ctalkington
|
/tests/components/roku/ @ctalkington
|
||||||
/homeassistant/components/romy/ @xeniter
|
/homeassistant/components/romy/ @xeniter
|
||||||
/tests/components/romy/ @xeniter
|
/tests/components/romy/ @xeniter
|
||||||
/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn @Xitee1 @Orhideous
|
/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
||||||
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Xitee1 @Orhideous
|
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
||||||
/homeassistant/components/roon/ @pavoni
|
/homeassistant/components/roon/ @pavoni
|
||||||
/tests/components/roon/ @pavoni
|
/tests/components/roon/ @pavoni
|
||||||
/homeassistant/components/rpi_power/ @shenxn @swetoast
|
/homeassistant/components/rpi_power/ @shenxn @swetoast
|
||||||
@ -1349,6 +1358,7 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/smarttub/ @mdz
|
/homeassistant/components/smarttub/ @mdz
|
||||||
/tests/components/smarttub/ @mdz
|
/tests/components/smarttub/ @mdz
|
||||||
/homeassistant/components/smarty/ @z0mbieprocess
|
/homeassistant/components/smarty/ @z0mbieprocess
|
||||||
|
/tests/components/smarty/ @z0mbieprocess
|
||||||
/homeassistant/components/smhi/ @gjohansson-ST
|
/homeassistant/components/smhi/ @gjohansson-ST
|
||||||
/tests/components/smhi/ @gjohansson-ST
|
/tests/components/smhi/ @gjohansson-ST
|
||||||
/homeassistant/components/smlight/ @tl-sl
|
/homeassistant/components/smlight/ @tl-sl
|
||||||
@ -1382,15 +1392,13 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/spaceapi/ @fabaff
|
/tests/components/spaceapi/ @fabaff
|
||||||
/homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87
|
/homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87
|
||||||
/tests/components/speedtestdotnet/ @rohankapoorcom @engrbm87
|
/tests/components/speedtestdotnet/ @rohankapoorcom @engrbm87
|
||||||
/homeassistant/components/spider/ @peternijssen
|
|
||||||
/tests/components/spider/ @peternijssen
|
|
||||||
/homeassistant/components/splunk/ @Bre77
|
/homeassistant/components/splunk/ @Bre77
|
||||||
/homeassistant/components/spotify/ @frenck @joostlek
|
/homeassistant/components/spotify/ @frenck @joostlek
|
||||||
/tests/components/spotify/ @frenck @joostlek
|
/tests/components/spotify/ @frenck @joostlek
|
||||||
/homeassistant/components/sql/ @gjohansson-ST @dougiteixeira
|
/homeassistant/components/sql/ @gjohansson-ST @dougiteixeira
|
||||||
/tests/components/sql/ @gjohansson-ST @dougiteixeira
|
/tests/components/sql/ @gjohansson-ST @dougiteixeira
|
||||||
/homeassistant/components/squeezebox/ @rajlaud
|
/homeassistant/components/squeezebox/ @rajlaud @pssc @peteS-UK
|
||||||
/tests/components/squeezebox/ @rajlaud
|
/tests/components/squeezebox/ @rajlaud @pssc @peteS-UK
|
||||||
/homeassistant/components/srp_energy/ @briglx
|
/homeassistant/components/srp_energy/ @briglx
|
||||||
/tests/components/srp_energy/ @briglx
|
/tests/components/srp_energy/ @briglx
|
||||||
/homeassistant/components/starline/ @anonym-tsk
|
/homeassistant/components/starline/ @anonym-tsk
|
||||||
@ -1414,8 +1422,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/stt/ @home-assistant/core
|
/tests/components/stt/ @home-assistant/core
|
||||||
/homeassistant/components/subaru/ @G-Two
|
/homeassistant/components/subaru/ @G-Two
|
||||||
/tests/components/subaru/ @G-Two
|
/tests/components/subaru/ @G-Two
|
||||||
/homeassistant/components/suez_water/ @ooii
|
/homeassistant/components/suez_water/ @ooii @jb101010-2
|
||||||
/tests/components/suez_water/ @ooii
|
/tests/components/suez_water/ @ooii @jb101010-2
|
||||||
/homeassistant/components/sun/ @Swamp-Ig
|
/homeassistant/components/sun/ @Swamp-Ig
|
||||||
/tests/components/sun/ @Swamp-Ig
|
/tests/components/sun/ @Swamp-Ig
|
||||||
/homeassistant/components/sunweg/ @rokam
|
/homeassistant/components/sunweg/ @rokam
|
||||||
|
17
Dockerfile
17
Dockerfile
@ -12,7 +12,7 @@ ENV \
|
|||||||
ARG QEMU_CPU
|
ARG QEMU_CPU
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
RUN pip3 install uv==0.4.15
|
RUN pip3 install uv==0.4.28
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
@ -44,4 +44,19 @@ RUN \
|
|||||||
# Home Assistant S6-Overlay
|
# Home Assistant S6-Overlay
|
||||||
COPY rootfs /
|
COPY rootfs /
|
||||||
|
|
||||||
|
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
||||||
|
ARG BUILD_ARCH
|
||||||
|
# Get go2rtc binary
|
||||||
|
RUN \
|
||||||
|
case "${BUILD_ARCH}" in \
|
||||||
|
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||||
|
"armhf") go2rtc_suffix='armv6' ;; \
|
||||||
|
"armv7") go2rtc_suffix='arm' ;; \
|
||||||
|
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||||
|
esac \
|
||||||
|
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.6/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||||
|
&& chmod +x /bin/go2rtc \
|
||||||
|
# Verify go2rtc can be executed
|
||||||
|
&& go2rtc --version
|
||||||
|
|
||||||
WORKDIR /config
|
WORKDIR /config
|
||||||
|
@ -7,8 +7,6 @@ Check out `home-assistant.io <https://home-assistant.io>`__ for `a
|
|||||||
demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
||||||
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
||||||
|
|
||||||
This is a project of the `Open Home Foundation <https://www.openhomefoundation.org/>`__.
|
|
||||||
|
|
||||||
|screenshot-states|
|
|screenshot-states|
|
||||||
|
|
||||||
Featured integrations
|
Featured integrations
|
||||||
@ -22,9 +20,14 @@ components <https://developers.home-assistant.io/docs/creating_component_index/>
|
|||||||
If you run into issues while using Home Assistant or during development
|
If you run into issues while using Home Assistant or during development
|
||||||
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
||||||
|
|
||||||
|
|ohf-logo|
|
||||||
|
|
||||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||||
:target: https://www.home-assistant.io/join-chat/
|
:target: https://www.home-assistant.io/join-chat/
|
||||||
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png
|
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png
|
||||||
:target: https://demo.home-assistant.io
|
:target: https://demo.home-assistant.io
|
||||||
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
|
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
|
||||||
:target: https://home-assistant.io/integrations/
|
:target: https://home-assistant.io/integrations/
|
||||||
|
.. |ohf-logo| image:: https://www.openhomefoundation.org/badges/home-assistant.png
|
||||||
|
:alt: Home Assistant - A project from the Open Home Foundation
|
||||||
|
:target: https://www.openhomefoundation.org/
|
||||||
|
@ -9,6 +9,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
from .backup_restore import restore_backup
|
||||||
from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||||
|
|
||||||
FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
||||||
@ -182,6 +183,9 @@ def main() -> int:
|
|||||||
return scripts.run(args.script)
|
return scripts.run(args.script)
|
||||||
|
|
||||||
config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config))
|
config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config))
|
||||||
|
if restore_backup(config_dir):
|
||||||
|
return RESTART_EXIT_CODE
|
||||||
|
|
||||||
ensure_config_path(config_dir)
|
ensure_config_path(config_dir)
|
||||||
|
|
||||||
# pylint: disable-next=import-outside-toplevel
|
# pylint: disable-next=import-outside-toplevel
|
||||||
|
@ -12,7 +12,6 @@ from typing import Any, cast
|
|||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
from homeassistant import data_entry_flow
|
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
CALLBACK_TYPE,
|
CALLBACK_TYPE,
|
||||||
HassJob,
|
HassJob,
|
||||||
@ -20,13 +19,14 @@ from homeassistant.core import (
|
|||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
callback,
|
callback,
|
||||||
)
|
)
|
||||||
|
from homeassistant.data_entry_flow import FlowHandler, FlowManager, FlowResultType
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import auth_store, jwt_wrapper, models
|
from . import auth_store, jwt_wrapper, models
|
||||||
from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRATION
|
from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRATION
|
||||||
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
|
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
|
||||||
from .models import AuthFlowResult
|
from .models import AuthFlowContext, AuthFlowResult
|
||||||
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
||||||
from .providers.homeassistant import HassAuthProvider
|
from .providers.homeassistant import HassAuthProvider
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ async def auth_manager_from_config(
|
|||||||
|
|
||||||
|
|
||||||
class AuthManagerFlowManager(
|
class AuthManagerFlowManager(
|
||||||
data_entry_flow.FlowManager[AuthFlowResult, tuple[str, str]]
|
FlowManager[AuthFlowContext, AuthFlowResult, tuple[str, str]]
|
||||||
):
|
):
|
||||||
"""Manage authentication flows."""
|
"""Manage authentication flows."""
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ class AuthManagerFlowManager(
|
|||||||
self,
|
self,
|
||||||
handler_key: tuple[str, str],
|
handler_key: tuple[str, str],
|
||||||
*,
|
*,
|
||||||
context: dict[str, Any] | None = None,
|
context: AuthFlowContext | None = None,
|
||||||
data: dict[str, Any] | None = None,
|
data: dict[str, Any] | None = None,
|
||||||
) -> LoginFlow:
|
) -> LoginFlow:
|
||||||
"""Create a login flow."""
|
"""Create a login flow."""
|
||||||
@ -124,7 +124,7 @@ class AuthManagerFlowManager(
|
|||||||
|
|
||||||
async def async_finish_flow(
|
async def async_finish_flow(
|
||||||
self,
|
self,
|
||||||
flow: data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]],
|
flow: FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]],
|
||||||
result: AuthFlowResult,
|
result: AuthFlowResult,
|
||||||
) -> AuthFlowResult:
|
) -> AuthFlowResult:
|
||||||
"""Return a user as result of login flow.
|
"""Return a user as result of login flow.
|
||||||
@ -134,7 +134,7 @@ class AuthManagerFlowManager(
|
|||||||
"""
|
"""
|
||||||
flow = cast(LoginFlow, flow)
|
flow = cast(LoginFlow, flow)
|
||||||
|
|
||||||
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
if result["type"] != FlowResultType.CREATE_ENTRY:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# we got final result
|
# we got final result
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import cached_property
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Any, NamedTuple
|
from typing import Any, NamedTuple
|
||||||
import uuid
|
import uuid
|
||||||
@ -11,9 +11,10 @@ import uuid
|
|||||||
import attr
|
import attr
|
||||||
from attr import Attribute
|
from attr import Attribute
|
||||||
from attr.setters import validate
|
from attr.setters import validate
|
||||||
|
from propcache import cached_property
|
||||||
|
|
||||||
from homeassistant.const import __version__
|
from homeassistant.const import __version__
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowContext, FlowResult
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import permissions as perm_mdl
|
from . import permissions as perm_mdl
|
||||||
@ -23,7 +24,16 @@ TOKEN_TYPE_NORMAL = "normal"
|
|||||||
TOKEN_TYPE_SYSTEM = "system"
|
TOKEN_TYPE_SYSTEM = "system"
|
||||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
|
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
|
||||||
|
|
||||||
AuthFlowResult = FlowResult[tuple[str, str]]
|
|
||||||
|
class AuthFlowContext(FlowContext, total=False):
|
||||||
|
"""Typed context dict for auth flow."""
|
||||||
|
|
||||||
|
credential_only: bool
|
||||||
|
ip_address: IPv4Address | IPv6Address
|
||||||
|
redirect_uri: str
|
||||||
|
|
||||||
|
|
||||||
|
AuthFlowResult = FlowResult[AuthFlowContext, tuple[str, str]]
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
|
@ -10,9 +10,10 @@ from typing import Any
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
from homeassistant import data_entry_flow, requirements
|
from homeassistant import requirements
|
||||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.data_entry_flow import FlowHandler
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.importlib import async_import_module
|
from homeassistant.helpers.importlib import async_import_module
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
@ -21,7 +22,14 @@ from homeassistant.util.hass_dict import HassKey
|
|||||||
|
|
||||||
from ..auth_store import AuthStore
|
from ..auth_store import AuthStore
|
||||||
from ..const import MFA_SESSION_EXPIRATION
|
from ..const import MFA_SESSION_EXPIRATION
|
||||||
from ..models import AuthFlowResult, Credentials, RefreshToken, User, UserMeta
|
from ..models import (
|
||||||
|
AuthFlowContext,
|
||||||
|
AuthFlowResult,
|
||||||
|
Credentials,
|
||||||
|
RefreshToken,
|
||||||
|
User,
|
||||||
|
UserMeta,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DATA_REQS: HassKey[set[str]] = HassKey("auth_prov_reqs_processed")
|
DATA_REQS: HassKey[set[str]] = HassKey("auth_prov_reqs_processed")
|
||||||
@ -97,7 +105,7 @@ class AuthProvider:
|
|||||||
|
|
||||||
# Implement by extending class
|
# Implement by extending class
|
||||||
|
|
||||||
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow:
|
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
||||||
"""Return the data flow for logging in with auth provider.
|
"""Return the data flow for logging in with auth provider.
|
||||||
|
|
||||||
Auth provider should extend LoginFlow and return an instance.
|
Auth provider should extend LoginFlow and return an instance.
|
||||||
@ -184,7 +192,7 @@ async def load_auth_provider_module(
|
|||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
class LoginFlow(data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]]):
|
class LoginFlow(FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]]):
|
||||||
"""Handler for the login flow."""
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
_flow_result = AuthFlowResult
|
_flow_result = AuthFlowResult
|
||||||
|
@ -13,7 +13,7 @@ import voluptuous as vol
|
|||||||
from homeassistant.const import CONF_COMMAND
|
from homeassistant.const import CONF_COMMAND
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
from ..models import AuthFlowResult, Credentials, UserMeta
|
from ..models import AuthFlowContext, AuthFlowResult, Credentials, UserMeta
|
||||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||||
|
|
||||||
CONF_ARGS = "args"
|
CONF_ARGS = "args"
|
||||||
@ -59,7 +59,7 @@ class CommandLineAuthProvider(AuthProvider):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._user_meta: dict[str, dict[str, Any]] = {}
|
self._user_meta: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow:
|
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
return CommandLineLoginFlow(self)
|
return CommandLineLoginFlow(self)
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.helpers import issue_registry as ir
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
|
|
||||||
from ..models import AuthFlowResult, Credentials, UserMeta
|
from ..models import AuthFlowContext, AuthFlowResult, Credentials, UserMeta
|
||||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||||
|
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
@ -305,7 +305,7 @@ class HassAuthProvider(AuthProvider):
|
|||||||
await data.async_load()
|
await data.async_load()
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow:
|
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
return HassLoginFlow(self)
|
return HassLoginFlow(self)
|
||||||
|
|
||||||
|
@ -4,14 +4,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
import hmac
|
import hmac
|
||||||
from typing import Any, cast
|
from typing import cast
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
from ..models import AuthFlowResult, Credentials, UserMeta
|
from ..models import AuthFlowContext, AuthFlowResult, Credentials, UserMeta
|
||||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||||
|
|
||||||
USER_SCHEMA = vol.Schema(
|
USER_SCHEMA = vol.Schema(
|
||||||
@ -36,7 +36,7 @@ class InvalidAuthError(HomeAssistantError):
|
|||||||
class ExampleAuthProvider(AuthProvider):
|
class ExampleAuthProvider(AuthProvider):
|
||||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||||
|
|
||||||
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow:
|
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
return ExampleLoginFlow(self)
|
return ExampleLoginFlow(self)
|
||||||
|
|
||||||
|
@ -25,7 +25,13 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.helpers.network import is_cloud_connection
|
from homeassistant.helpers.network import is_cloud_connection
|
||||||
|
|
||||||
from .. import InvalidAuthError
|
from .. import InvalidAuthError
|
||||||
from ..models import AuthFlowResult, Credentials, RefreshToken, UserMeta
|
from ..models import (
|
||||||
|
AuthFlowContext,
|
||||||
|
AuthFlowResult,
|
||||||
|
Credentials,
|
||||||
|
RefreshToken,
|
||||||
|
UserMeta,
|
||||||
|
)
|
||||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||||
|
|
||||||
type IPAddress = IPv4Address | IPv6Address
|
type IPAddress = IPv4Address | IPv6Address
|
||||||
@ -98,7 +104,7 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
|||||||
"""Trusted Networks auth provider does not support MFA."""
|
"""Trusted Networks auth provider does not support MFA."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow:
|
async def async_login_flow(self, context: AuthFlowContext | None) -> LoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
assert context is not None
|
assert context is not None
|
||||||
ip_addr = cast(IPAddress, context.get("ip_address"))
|
ip_addr = cast(IPAddress, context.get("ip_address"))
|
||||||
|
@ -9,6 +9,7 @@ import it.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# pylint: disable-next=hass-deprecated-import
|
||||||
from functools import cached_property as _cached_property, partial
|
from functools import cached_property as _cached_property, partial
|
||||||
|
|
||||||
from homeassistant.helpers.deprecation import (
|
from homeassistant.helpers.deprecation import (
|
||||||
|
126
homeassistant/backup_restore.py
Normal file
126
homeassistant/backup_restore.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
"""Home Assistant module to handle restoring backups."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
from awesomeversion import AwesomeVersion
|
||||||
|
import securetar
|
||||||
|
|
||||||
|
from .const import __version__ as HA_VERSION
|
||||||
|
|
||||||
|
RESTORE_BACKUP_FILE = ".HA_RESTORE"
|
||||||
|
KEEP_PATHS = ("backups",)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RestoreBackupFileContent:
|
||||||
|
"""Definition for restore backup file content."""
|
||||||
|
|
||||||
|
backup_file_path: Path
|
||||||
|
|
||||||
|
|
||||||
|
def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent | None:
|
||||||
|
"""Return the contents of the restore backup file."""
|
||||||
|
instruction_path = config_dir.joinpath(RESTORE_BACKUP_FILE)
|
||||||
|
try:
|
||||||
|
instruction_content = instruction_path.read_text(encoding="utf-8")
|
||||||
|
return RestoreBackupFileContent(
|
||||||
|
backup_file_path=Path(instruction_content.split(";")[0])
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_configuration_directory(config_dir: Path) -> None:
|
||||||
|
"""Delete all files and directories in the config directory except for the backups directory."""
|
||||||
|
keep_paths = [config_dir.joinpath(path) for path in KEEP_PATHS]
|
||||||
|
config_contents = sorted(
|
||||||
|
[entry for entry in config_dir.iterdir() if entry not in keep_paths]
|
||||||
|
)
|
||||||
|
|
||||||
|
for entry in config_contents:
|
||||||
|
entrypath = config_dir.joinpath(entry)
|
||||||
|
|
||||||
|
if entrypath.is_file():
|
||||||
|
entrypath.unlink()
|
||||||
|
elif entrypath.is_dir():
|
||||||
|
shutil.rmtree(entrypath)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_backup(config_dir: Path, backup_file_path: Path) -> None:
|
||||||
|
"""Extract the backup file to the config directory."""
|
||||||
|
with (
|
||||||
|
TemporaryDirectory() as tempdir,
|
||||||
|
securetar.SecureTarFile(
|
||||||
|
backup_file_path,
|
||||||
|
gzip=False,
|
||||||
|
mode="r",
|
||||||
|
) as ostf,
|
||||||
|
):
|
||||||
|
ostf.extractall(
|
||||||
|
path=Path(tempdir, "extracted"),
|
||||||
|
members=securetar.secure_path(ostf),
|
||||||
|
filter="fully_trusted",
|
||||||
|
)
|
||||||
|
backup_meta_file = Path(tempdir, "extracted", "backup.json")
|
||||||
|
backup_meta = json.loads(backup_meta_file.read_text(encoding="utf8"))
|
||||||
|
|
||||||
|
if (
|
||||||
|
backup_meta_version := AwesomeVersion(
|
||||||
|
backup_meta["homeassistant"]["version"]
|
||||||
|
)
|
||||||
|
) > HA_VERSION:
|
||||||
|
raise ValueError(
|
||||||
|
f"You need at least Home Assistant version {backup_meta_version} to restore this backup"
|
||||||
|
)
|
||||||
|
|
||||||
|
with securetar.SecureTarFile(
|
||||||
|
Path(
|
||||||
|
tempdir,
|
||||||
|
"extracted",
|
||||||
|
f"homeassistant.tar{'.gz' if backup_meta["compressed"] else ''}",
|
||||||
|
),
|
||||||
|
gzip=backup_meta["compressed"],
|
||||||
|
mode="r",
|
||||||
|
) as istf:
|
||||||
|
for member in istf.getmembers():
|
||||||
|
if member.name == "data":
|
||||||
|
continue
|
||||||
|
member.name = member.name.replace("data/", "")
|
||||||
|
_clear_configuration_directory(config_dir)
|
||||||
|
istf.extractall(
|
||||||
|
path=config_dir,
|
||||||
|
members=[
|
||||||
|
member
|
||||||
|
for member in securetar.secure_path(istf)
|
||||||
|
if member.name != "data"
|
||||||
|
],
|
||||||
|
filter="fully_trusted",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def restore_backup(config_dir_path: str) -> bool:
|
||||||
|
"""Restore the backup file if any.
|
||||||
|
|
||||||
|
Returns True if a restore backup file was found and restored, False otherwise.
|
||||||
|
"""
|
||||||
|
config_dir = Path(config_dir_path)
|
||||||
|
if not (restore_content := restore_backup_file_content(config_dir)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
||||||
|
backup_file_path = restore_content.backup_file_path
|
||||||
|
_LOGGER.info("Restoring %s", backup_file_path)
|
||||||
|
try:
|
||||||
|
_extract_backup(config_dir, backup_file_path)
|
||||||
|
except FileNotFoundError as err:
|
||||||
|
raise ValueError(f"Backup file {backup_file_path} does not exist") from err
|
||||||
|
_LOGGER.info("Restore complete, restarting")
|
||||||
|
return True
|
@ -70,6 +70,7 @@ from .const import (
|
|||||||
REQUIRED_NEXT_PYTHON_VER,
|
REQUIRED_NEXT_PYTHON_VER,
|
||||||
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
||||||
)
|
)
|
||||||
|
from .core_config import async_process_ha_core_config
|
||||||
from .exceptions import HomeAssistantError
|
from .exceptions import HomeAssistantError
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
area_registry,
|
area_registry,
|
||||||
@ -479,7 +480,7 @@ async def async_from_config_dict(
|
|||||||
core_config = config.get(core.DOMAIN, {})
|
core_config = config.get(core.DOMAIN, {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await conf_util.async_process_ha_core_config(hass, core_config)
|
await async_process_ha_core_config(hass, core_config)
|
||||||
except vol.Invalid as config_err:
|
except vol.Invalid as config_err:
|
||||||
conf_util.async_log_schema_error(config_err, core.DOMAIN, core_config, hass)
|
conf_util.async_log_schema_error(config_err, core.DOMAIN, core_config, hass)
|
||||||
async_notify_setup_error(hass, core.DOMAIN)
|
async_notify_setup_error(hass, core.DOMAIN)
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
"google_assistant",
|
"google_assistant",
|
||||||
"google_assistant_sdk",
|
"google_assistant_sdk",
|
||||||
"google_cloud",
|
"google_cloud",
|
||||||
"google_domains",
|
|
||||||
"google_generative_ai_conversation",
|
"google_generative_ai_conversation",
|
||||||
"google_mail",
|
"google_mail",
|
||||||
"google_maps",
|
"google_maps",
|
||||||
|
5
homeassistant/brands/husqvarna.json
Normal file
5
homeassistant/brands/husqvarna.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "husqvarna",
|
||||||
|
"name": "Husqvarna",
|
||||||
|
"integrations": ["husqvarna_automower", "husqvarna_automower_ble"]
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "lg",
|
"domain": "lg",
|
||||||
"name": "LG",
|
"name": "LG",
|
||||||
"integrations": ["lg_netcast", "lg_soundbar", "webostv"]
|
"integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from jaraco.abode.client import Client as Abode
|
from jaraco.abode.client import Client as Abode
|
||||||
|
import jaraco.abode.config
|
||||||
from jaraco.abode.exceptions import (
|
from jaraco.abode.exceptions import (
|
||||||
AuthenticationException as AbodeAuthenticationException,
|
AuthenticationException as AbodeAuthenticationException,
|
||||||
Exception as AbodeException,
|
Exception as AbodeException,
|
||||||
@ -93,6 +95,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
password = entry.data[CONF_PASSWORD]
|
password = entry.data[CONF_PASSWORD]
|
||||||
polling = entry.data[CONF_POLLING]
|
polling = entry.data[CONF_POLLING]
|
||||||
|
|
||||||
|
# Configure abode library to use config directory for storing data
|
||||||
|
jaraco.abode.config.paths.override(user_data=Path(hass.config.path("Abode")))
|
||||||
|
|
||||||
# For previous config entries where unique_id is None
|
# For previous config entries where unique_id is None
|
||||||
if entry.unique_id is None:
|
if entry.unique_id is None:
|
||||||
hass.config_entries.async_update_entry(
|
hass.config_entries.async_update_entry(
|
||||||
|
@ -7,13 +7,9 @@ from jaraco.abode.devices.alarm import Alarm
|
|||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
AlarmControlPanelEntity,
|
AlarmControlPanelEntity,
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
|
AlarmControlPanelState,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
@ -44,14 +40,14 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanelEntity):
|
|||||||
_device: Alarm
|
_device: Alarm
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str | None:
|
def alarm_state(self) -> AlarmControlPanelState | None:
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
if self._device.is_standby:
|
if self._device.is_standby:
|
||||||
return STATE_ALARM_DISARMED
|
return AlarmControlPanelState.DISARMED
|
||||||
if self._device.is_away:
|
if self._device.is_away:
|
||||||
return STATE_ALARM_ARMED_AWAY
|
return AlarmControlPanelState.ARMED_AWAY
|
||||||
if self._device.is_home:
|
if self._device.is_home:
|
||||||
return STATE_ALARM_ARMED_HOME
|
return AlarmControlPanelState.ARMED_HOME
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def alarm_disarm(self, code: str | None = None) -> None:
|
def alarm_disarm(self, code: str | None = None) -> None:
|
||||||
|
@ -102,15 +102,7 @@ class AbodeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
existing_entry = await self.async_set_unique_id(self._username)
|
existing_entry = await self.async_set_unique_id(self._username)
|
||||||
|
|
||||||
if existing_entry:
|
if existing_entry:
|
||||||
self.hass.config_entries.async_update_entry(
|
return self.async_update_reload_and_abort(existing_entry, data=config_data)
|
||||||
existing_entry, data=config_data
|
|
||||||
)
|
|
||||||
# Reload the Abode config entry otherwise devices will remain unavailable
|
|
||||||
self.hass.async_create_task(
|
|
||||||
self.hass.config_entries.async_reload(existing_entry.entry_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_abort(reason="reauth_successful")
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=cast(str, self._username), data=config_data
|
title=cast(str, self._username), data=config_data
|
||||||
|
@ -9,5 +9,5 @@
|
|||||||
},
|
},
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["jaraco.abode", "lomond"],
|
"loggers": ["jaraco.abode", "lomond"],
|
||||||
"requirements": ["jaraco.abode==6.2.0"]
|
"requirements": ["jaraco.abode==6.2.1"]
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from accuweather import AccuWeather
|
from accuweather import AccuWeather
|
||||||
|
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
|
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
|
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
@ -16,7 +14,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
|
|
||||||
from .const import DOMAIN, UPDATE_INTERVAL_DAILY_FORECAST, UPDATE_INTERVAL_OBSERVATION
|
from .const import DOMAIN, UPDATE_INTERVAL_DAILY_FORECAST, UPDATE_INTERVAL_OBSERVATION
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
|
AccuWeatherConfigEntry,
|
||||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||||
|
AccuWeatherData,
|
||||||
AccuWeatherObservationDataUpdateCoordinator,
|
AccuWeatherObservationDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -25,17 +25,6 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AccuWeatherData:
|
|
||||||
"""Data for AccuWeather integration."""
|
|
||||||
|
|
||||||
coordinator_observation: AccuWeatherObservationDataUpdateCoordinator
|
|
||||||
coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
type AccuWeatherConfigEntry = ConfigEntry[AccuWeatherData]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) -> bool:
|
||||||
"""Set up AccuWeather as config entry."""
|
"""Set up AccuWeather as config entry."""
|
||||||
api_key: str = entry.data[CONF_API_KEY]
|
api_key: str = entry.data[CONF_API_KEY]
|
||||||
@ -50,6 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry)
|
|||||||
|
|
||||||
coordinator_observation = AccuWeatherObservationDataUpdateCoordinator(
|
coordinator_observation = AccuWeatherObservationDataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
|
entry,
|
||||||
accuweather,
|
accuweather,
|
||||||
name,
|
name,
|
||||||
"observation",
|
"observation",
|
||||||
@ -58,6 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry)
|
|||||||
|
|
||||||
coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator(
|
coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
|
entry,
|
||||||
accuweather,
|
accuweather,
|
||||||
name,
|
name,
|
||||||
"daily forecast",
|
"daily forecast",
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
"""The AccuWeather coordinator."""
|
"""The AccuWeather coordinator."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from asyncio import timeout
|
from asyncio import timeout
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
@ -8,6 +11,7 @@ from typing import TYPE_CHECKING, Any
|
|||||||
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||||
from aiohttp.client_exceptions import ClientConnectorError
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import (
|
||||||
@ -23,6 +27,17 @@ EXCEPTIONS = (ApiError, ClientConnectorError, InvalidApiKeyError, RequestsExceed
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AccuWeatherData:
|
||||||
|
"""Data for AccuWeather integration."""
|
||||||
|
|
||||||
|
coordinator_observation: AccuWeatherObservationDataUpdateCoordinator
|
||||||
|
coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
type AccuWeatherConfigEntry = ConfigEntry[AccuWeatherData]
|
||||||
|
|
||||||
|
|
||||||
class AccuWeatherObservationDataUpdateCoordinator(
|
class AccuWeatherObservationDataUpdateCoordinator(
|
||||||
DataUpdateCoordinator[dict[str, Any]]
|
DataUpdateCoordinator[dict[str, Any]]
|
||||||
):
|
):
|
||||||
@ -31,6 +46,7 @@ class AccuWeatherObservationDataUpdateCoordinator(
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
config_entry: AccuWeatherConfigEntry,
|
||||||
accuweather: AccuWeather,
|
accuweather: AccuWeather,
|
||||||
name: str,
|
name: str,
|
||||||
coordinator_type: str,
|
coordinator_type: str,
|
||||||
@ -48,6 +64,7 @@ class AccuWeatherObservationDataUpdateCoordinator(
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
name=f"{name} ({coordinator_type})",
|
name=f"{name} ({coordinator_type})",
|
||||||
update_interval=update_interval,
|
update_interval=update_interval,
|
||||||
)
|
)
|
||||||
@ -73,6 +90,7 @@ class AccuWeatherDailyForecastDataUpdateCoordinator(
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
config_entry: AccuWeatherConfigEntry,
|
||||||
accuweather: AccuWeather,
|
accuweather: AccuWeather,
|
||||||
name: str,
|
name: str,
|
||||||
coordinator_type: str,
|
coordinator_type: str,
|
||||||
@ -90,6 +108,7 @@ class AccuWeatherDailyForecastDataUpdateCoordinator(
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
name=f"{name} ({coordinator_type})",
|
name=f"{name} ({coordinator_type})",
|
||||||
update_interval=update_interval,
|
update_interval=update_interval,
|
||||||
)
|
)
|
||||||
|
@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data
|
|||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import AccuWeatherConfigEntry, AccuWeatherData
|
from .coordinator import AccuWeatherConfigEntry, AccuWeatherData
|
||||||
|
|
||||||
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE}
|
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE}
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import AccuWeatherConfigEntry
|
|
||||||
from .const import (
|
from .const import (
|
||||||
API_METRIC,
|
API_METRIC,
|
||||||
ATTR_CATEGORY,
|
ATTR_CATEGORY,
|
||||||
@ -41,6 +40,7 @@ from .const import (
|
|||||||
MAX_FORECAST_DAYS,
|
MAX_FORECAST_DAYS,
|
||||||
)
|
)
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
|
AccuWeatherConfigEntry,
|
||||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||||
AccuWeatherObservationDataUpdateCoordinator,
|
AccuWeatherObservationDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
|
@ -9,8 +9,8 @@ from accuweather.const import ENDPOINT
|
|||||||
from homeassistant.components import system_health
|
from homeassistant.components import system_health
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
from . import AccuWeatherConfigEntry
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AccuWeatherConfigEntry
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -33,7 +33,6 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util.dt import utc_from_timestamp
|
from homeassistant.util.dt import utc_from_timestamp
|
||||||
|
|
||||||
from . import AccuWeatherConfigEntry, AccuWeatherData
|
|
||||||
from .const import (
|
from .const import (
|
||||||
API_METRIC,
|
API_METRIC,
|
||||||
ATTR_DIRECTION,
|
ATTR_DIRECTION,
|
||||||
@ -43,7 +42,9 @@ from .const import (
|
|||||||
CONDITION_MAP,
|
CONDITION_MAP,
|
||||||
)
|
)
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
|
AccuWeatherConfigEntry,
|
||||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||||
|
AccuWeatherData,
|
||||||
AccuWeatherObservationDataUpdateCoordinator,
|
AccuWeatherObservationDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ from typing import Any
|
|||||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.hassio import HassioServiceInfo
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
@ -18,6 +17,7 @@ from homeassistant.const import (
|
|||||||
CONF_VERIFY_SSL,
|
CONF_VERIFY_SSL,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
@ -55,6 +55,7 @@ async def async_setup_entry(
|
|||||||
coordinator = DataUpdateCoordinator(
|
coordinator = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
name="Advantage Air",
|
name="Advantage Air",
|
||||||
update_method=async_get,
|
update_method=async_get,
|
||||||
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
|
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
|
||||||
|
@ -5,12 +5,7 @@ from __future__ import annotations
|
|||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
AlarmControlPanelEntity,
|
AlarmControlPanelEntity,
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
)
|
AlarmControlPanelState,
|
||||||
from homeassistant.const import (
|
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
@ -65,37 +60,37 @@ class AgentBaseStation(AlarmControlPanelEntity):
|
|||||||
self._attr_available = self._client.is_available
|
self._attr_available = self._client.is_available
|
||||||
armed = self._client.is_armed
|
armed = self._client.is_armed
|
||||||
if armed is None:
|
if armed is None:
|
||||||
self._attr_state = None
|
self._attr_alarm_state = None
|
||||||
return
|
return
|
||||||
if armed:
|
if armed:
|
||||||
prof = (await self._client.get_active_profile()).lower()
|
prof = (await self._client.get_active_profile()).lower()
|
||||||
self._attr_state = STATE_ALARM_ARMED_AWAY
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
|
||||||
if prof == CONF_HOME_MODE_NAME:
|
if prof == CONF_HOME_MODE_NAME:
|
||||||
self._attr_state = STATE_ALARM_ARMED_HOME
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
|
||||||
elif prof == CONF_NIGHT_MODE_NAME:
|
elif prof == CONF_NIGHT_MODE_NAME:
|
||||||
self._attr_state = STATE_ALARM_ARMED_NIGHT
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
|
||||||
else:
|
else:
|
||||||
self._attr_state = STATE_ALARM_DISARMED
|
self._attr_alarm_state = AlarmControlPanelState.DISARMED
|
||||||
|
|
||||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
await self._client.disarm()
|
await self._client.disarm()
|
||||||
self._attr_state = STATE_ALARM_DISARMED
|
self._attr_alarm_state = AlarmControlPanelState.DISARMED
|
||||||
|
|
||||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||||
"""Send arm away command. Uses custom mode."""
|
"""Send arm away command. Uses custom mode."""
|
||||||
await self._client.arm()
|
await self._client.arm()
|
||||||
await self._client.set_active_profile(CONF_AWAY_MODE_NAME)
|
await self._client.set_active_profile(CONF_AWAY_MODE_NAME)
|
||||||
self._attr_state = STATE_ALARM_ARMED_AWAY
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
|
||||||
|
|
||||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||||
"""Send arm home command. Uses custom mode."""
|
"""Send arm home command. Uses custom mode."""
|
||||||
await self._client.arm()
|
await self._client.arm()
|
||||||
await self._client.set_active_profile(CONF_HOME_MODE_NAME)
|
await self._client.set_active_profile(CONF_HOME_MODE_NAME)
|
||||||
self._attr_state = STATE_ALARM_ARMED_HOME
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
|
||||||
|
|
||||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||||
"""Send arm night command. Uses custom mode."""
|
"""Send arm night command. Uses custom mode."""
|
||||||
await self._client.arm()
|
await self._client.arm()
|
||||||
await self._client.set_active_profile(CONF_NIGHT_MODE_NAME)
|
await self._client.set_active_profile(CONF_NIGHT_MODE_NAME)
|
||||||
self._attr_state = STATE_ALARM_ARMED_NIGHT
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
|
||||||
|
@ -19,7 +19,7 @@ from .const import DOMAIN
|
|||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN_DATA: HassKey[EntityComponent[AirQualityEntity]] = HassKey(DOMAIN)
|
DATA_COMPONENT: HassKey[EntityComponent[AirQualityEntity]] = HassKey(DOMAIN)
|
||||||
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
||||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
||||||
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
||||||
@ -56,7 +56,7 @@ PROP_TO_ATTR: Final[dict[str, str]] = {
|
|||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the air quality component."""
|
"""Set up the air quality component."""
|
||||||
component = hass.data[DOMAIN_DATA] = EntityComponent[AirQualityEntity](
|
component = hass.data[DATA_COMPONENT] = EntityComponent[AirQualityEntity](
|
||||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||||
)
|
)
|
||||||
await component.async_setup(config)
|
await component.async_setup(config)
|
||||||
@ -65,12 +65,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up a config entry."""
|
"""Set up a config entry."""
|
||||||
return await hass.data[DOMAIN_DATA].async_setup_entry(entry)
|
return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
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.data[DOMAIN_DATA].async_unload_entry(entry)
|
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
class AirQualityEntity(Entity):
|
class AirQualityEntity(Entity):
|
||||||
|
@ -9,9 +9,10 @@ from typing import TYPE_CHECKING
|
|||||||
from airgradient import AirGradientClient, AirGradientError, Config, Measures
|
from airgradient import AirGradientClient, AirGradientError, Config, Measures
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import AirGradientConfigEntry
|
from . import AirGradientConfigEntry
|
||||||
@ -29,6 +30,7 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
|||||||
"""Class to manage fetching AirGradient data."""
|
"""Class to manage fetching AirGradient data."""
|
||||||
|
|
||||||
config_entry: AirGradientConfigEntry
|
config_entry: AirGradientConfigEntry
|
||||||
|
_current_version: str
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
|
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
|
||||||
"""Initialize coordinator."""
|
"""Initialize coordinator."""
|
||||||
@ -42,11 +44,27 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
|||||||
assert self.config_entry.unique_id
|
assert self.config_entry.unique_id
|
||||||
self.serial_number = self.config_entry.unique_id
|
self.serial_number = self.config_entry.unique_id
|
||||||
|
|
||||||
|
async def _async_setup(self) -> None:
|
||||||
|
"""Set up the coordinator."""
|
||||||
|
self._current_version = (
|
||||||
|
await self.client.get_current_measures()
|
||||||
|
).firmware_version
|
||||||
|
|
||||||
async def _async_update_data(self) -> AirGradientData:
|
async def _async_update_data(self) -> AirGradientData:
|
||||||
try:
|
try:
|
||||||
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(error) from error
|
||||||
else:
|
if measures.firmware_version != self._current_version:
|
||||||
return AirGradientData(measures, config)
|
device_registry = dr.async_get(self.hass)
|
||||||
|
device_entry = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, self.serial_number)}
|
||||||
|
)
|
||||||
|
assert device_entry
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device_entry.id,
|
||||||
|
sw_version=measures.firmware_version,
|
||||||
|
)
|
||||||
|
self._current_version = measures.firmware_version
|
||||||
|
return AirGradientData(measures, config)
|
||||||
|
18
homeassistant/components/airgradient/diagnostics.py
Normal file
18
homeassistant/components/airgradient/diagnostics.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
"""Diagnostics support for Airgradient."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import AirGradientConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_config_entry_diagnostics(
|
||||||
|
hass: HomeAssistant, entry: AirGradientConfigEntry
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return diagnostics for a config entry."""
|
||||||
|
|
||||||
|
return asdict(entry.runtime_data.data)
|
@ -6,6 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["airgradient==0.9.0"],
|
"requirements": ["airgradient==0.9.1"],
|
||||||
"zeroconf": ["_airgradient._tcp.local."]
|
"zeroconf": ["_airgradient._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""Airgradient Update platform."""
|
"""Airgradient Update platform."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import cached_property
|
|
||||||
|
from propcache import cached_property
|
||||||
|
|
||||||
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Config flow for AirNow integration."""
|
"""Config flow for AirNow integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -12,7 +14,6 @@ from homeassistant.config_entries import (
|
|||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
OptionsFlow,
|
OptionsFlow,
|
||||||
OptionsFlowWithConfigEntry,
|
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@ -120,12 +121,12 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
) -> OptionsFlow:
|
) -> AirNowOptionsFlowHandler:
|
||||||
"""Return the options flow."""
|
"""Return the options flow."""
|
||||||
return AirNowOptionsFlowHandler(config_entry)
|
return AirNowOptionsFlowHandler()
|
||||||
|
|
||||||
|
|
||||||
class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
class AirNowOptionsFlowHandler(OptionsFlow):
|
||||||
"""Handle an options flow for AirNow."""
|
"""Handle an options flow for AirNow."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
@ -136,12 +137,7 @@ class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
|||||||
return self.async_create_entry(data=user_input)
|
return self.async_create_entry(data=user_input)
|
||||||
|
|
||||||
options_schema = vol.Schema(
|
options_schema = vol.Schema(
|
||||||
{
|
{vol.Optional(CONF_RADIUS): vol.All(int, vol.Range(min=5))}
|
||||||
vol.Optional(CONF_RADIUS): vol.All(
|
|
||||||
int,
|
|
||||||
vol.Range(min=5),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
|
@ -42,6 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
|||||||
coordinator = DataUpdateCoordinator(
|
coordinator = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_method=_update_method,
|
update_method=_update_method,
|
||||||
update_interval=SCAN_INTERVAL,
|
update_interval=SCAN_INTERVAL,
|
||||||
|
@ -2,75 +2,27 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
|
|
||||||
from bleak_retry_connector import close_stale_connections_by_address
|
|
||||||
|
|
||||||
from homeassistant.components import bluetooth
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
||||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
|
||||||
|
|
||||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MAX_RETRIES_AFTER_STARTUP
|
from .const import MAX_RETRIES_AFTER_STARTUP
|
||||||
|
from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator[AirthingsDevice]
|
|
||||||
AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: AirthingsBLEConfigEntry
|
hass: HomeAssistant, entry: AirthingsBLEConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set up Airthings BLE device from a config entry."""
|
"""Set up Airthings BLE device from a config entry."""
|
||||||
hass.data.setdefault(DOMAIN, {})
|
coordinator = AirthingsBLEDataUpdateCoordinator(hass, entry)
|
||||||
address = entry.unique_id
|
|
||||||
|
|
||||||
is_metric = hass.config.units is METRIC_SYSTEM
|
|
||||||
assert address is not None
|
|
||||||
|
|
||||||
await close_stale_connections_by_address(address)
|
|
||||||
|
|
||||||
ble_device = bluetooth.async_ble_device_from_address(hass, address)
|
|
||||||
|
|
||||||
if not ble_device:
|
|
||||||
raise ConfigEntryNotReady(
|
|
||||||
f"Could not find Airthings device with address {address}"
|
|
||||||
)
|
|
||||||
|
|
||||||
airthings = AirthingsBluetoothDeviceData(_LOGGER, is_metric)
|
|
||||||
|
|
||||||
async def _async_update_method() -> AirthingsDevice:
|
|
||||||
"""Get data from Airthings BLE."""
|
|
||||||
try:
|
|
||||||
data = await airthings.update_device(ble_device)
|
|
||||||
except Exception as err:
|
|
||||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
coordinator: AirthingsBLEDataUpdateCoordinator = DataUpdateCoordinator(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
name=DOMAIN,
|
|
||||||
update_method=_async_update_method,
|
|
||||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
|
||||||
)
|
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
# Once its setup and we know we are not going to delay
|
# Once its setup and we know we are not going to delay
|
||||||
# the startup of Home Assistant, we can set the max attempts
|
# the startup of Home Assistant, we can set the max attempts
|
||||||
# to a higher value. If the first connection attempt fails,
|
# to a higher value. If the first connection attempt fails,
|
||||||
# Home Assistant's built-in retry logic will take over.
|
# Home Assistant's built-in retry logic will take over.
|
||||||
airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP)
|
coordinator.airthings.set_max_attempts(MAX_RETRIES_AFTER_STARTUP)
|
||||||
|
|
||||||
entry.runtime_data = coordinator
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
68
homeassistant/components/airthings_ble/coordinator.py
Normal file
68
homeassistant/components/airthings_ble/coordinator.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
"""The Airthings BLE integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
|
||||||
|
from bleak.backends.device import BLEDevice
|
||||||
|
from bleak_retry_connector import close_stale_connections_by_address
|
||||||
|
|
||||||
|
from homeassistant.components import bluetooth
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||||
|
|
||||||
|
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type AirthingsBLEConfigEntry = ConfigEntry[AirthingsBLEDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
|
||||||
|
"""Class to manage fetching Airthings BLE data."""
|
||||||
|
|
||||||
|
ble_device: BLEDevice
|
||||||
|
config_entry: AirthingsBLEConfigEntry
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entry: AirthingsBLEConfigEntry) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
self.airthings = AirthingsBluetoothDeviceData(
|
||||||
|
_LOGGER, hass.config.units is METRIC_SYSTEM
|
||||||
|
)
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_setup(self) -> None:
|
||||||
|
"""Set up the coordinator."""
|
||||||
|
address = self.config_entry.unique_id
|
||||||
|
|
||||||
|
assert address is not None
|
||||||
|
|
||||||
|
await close_stale_connections_by_address(address)
|
||||||
|
|
||||||
|
ble_device = bluetooth.async_ble_device_from_address(self.hass, address)
|
||||||
|
|
||||||
|
if not ble_device:
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
f"Could not find Airthings device with address {address}"
|
||||||
|
)
|
||||||
|
self.ble_device = ble_device
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> AirthingsDevice:
|
||||||
|
"""Get data from Airthings BLE."""
|
||||||
|
try:
|
||||||
|
data = await self.airthings.update_device(self.ble_device)
|
||||||
|
except Exception as err:
|
||||||
|
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||||
|
|
||||||
|
return data
|
@ -34,8 +34,8 @@ from homeassistant.helpers.typing import StateType
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||||
|
|
||||||
from . import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
|
|
||||||
from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE
|
from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE
|
||||||
|
from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -9,8 +9,6 @@ from homeassistant.const import CONF_HOST, Platform
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER]
|
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER]
|
||||||
|
|
||||||
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
|
type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
|
||||||
@ -19,8 +17,6 @@ type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient]
|
|||||||
async def async_setup_entry(hass: HomeAssistant, entry: Airtouch5ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: Airtouch5ConfigEntry) -> bool:
|
||||||
"""Set up Airtouch 5 from a config entry."""
|
"""Set up Airtouch 5 from a config entry."""
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
|
||||||
|
|
||||||
# Create API instance
|
# Create API instance
|
||||||
host = entry.data[CONF_HOST]
|
host = entry.data[CONF_HOST]
|
||||||
client = Airtouch5SimpleClient(host)
|
client = Airtouch5SimpleClient(host)
|
||||||
|
@ -204,6 +204,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
|
|||||||
coordinator = DataUpdateCoordinator(
|
coordinator = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
name=async_get_geography_id(entry.data),
|
name=async_get_geography_id(entry.data),
|
||||||
# We give a placeholder update interval in order to create the coordinator;
|
# We give a placeholder update interval in order to create the coordinator;
|
||||||
# then, below, we use the coordinator's presence (along with any other
|
# then, below, we use the coordinator's presence (along with any other
|
||||||
|
@ -16,7 +16,12 @@ from pyairvisual.cloud_api import (
|
|||||||
from pyairvisual.errors import AirVisualError
|
from pyairvisual.errors import AirVisualError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import (
|
||||||
|
SOURCE_REAUTH,
|
||||||
|
ConfigEntry,
|
||||||
|
ConfigFlow,
|
||||||
|
ConfigFlowResult,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
CONF_COUNTRY,
|
CONF_COUNTRY,
|
||||||
@ -140,12 +145,11 @@ class AirVisualFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
valid_keys.add(user_input[CONF_API_KEY])
|
valid_keys.add(user_input[CONF_API_KEY])
|
||||||
|
|
||||||
if existing_entry := await self.async_set_unique_id(self._geo_id):
|
if self.source == SOURCE_REAUTH:
|
||||||
self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
|
return self.async_update_reload_and_abort(
|
||||||
self.hass.async_create_task(
|
self._get_reauth_entry(),
|
||||||
self.hass.config_entries.async_reload(existing_entry.entry_id)
|
data_updates={CONF_API_KEY: user_input[CONF_API_KEY]},
|
||||||
)
|
)
|
||||||
return self.async_abort(reason="reauth_successful")
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=f"Cloud API ({self._geo_id})",
|
title=f"Cloud API ({self._geo_id})",
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"general_error": "[%key:common::config_flow::error::unknown%]",
|
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||||
"location_not_found": "Location not found",
|
"location_not_found": "Location not found",
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
|
@ -81,6 +81,7 @@ async def async_setup_entry(
|
|||||||
coordinator = DataUpdateCoordinator(
|
coordinator = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
name="Node/Pro data",
|
name="Node/Pro data",
|
||||||
update_interval=UPDATE_INTERVAL,
|
update_interval=UPDATE_INTERVAL,
|
||||||
update_method=async_get_data,
|
update_method=async_get_data,
|
||||||
|
@ -14,7 +14,7 @@ from pyairvisual.node import (
|
|||||||
)
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
|
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
@ -76,9 +76,7 @@ class AirVisualProFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
def __init__(self) -> None:
|
_reauth_entry_data: Mapping[str, Any]
|
||||||
"""Initialize."""
|
|
||||||
self._reauth_entry: ConfigEntry | None = None
|
|
||||||
|
|
||||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||||
"""Import a config entry from `airvisual` integration (see #83882)."""
|
"""Import a config entry from `airvisual` integration (see #83882)."""
|
||||||
@ -88,9 +86,7 @@ class AirVisualProFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
self, entry_data: Mapping[str, Any]
|
self, entry_data: Mapping[str, Any]
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle configuration by re-auth."""
|
"""Handle configuration by re-auth."""
|
||||||
self._reauth_entry = self.hass.config_entries.async_get_entry(
|
self._reauth_entry_data = entry_data
|
||||||
self.context["entry_id"]
|
|
||||||
)
|
|
||||||
return await self.async_step_reauth_confirm()
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
async def async_step_reauth_confirm(
|
||||||
@ -102,10 +98,8 @@ class AirVisualProFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
step_id="reauth_confirm", data_schema=STEP_REAUTH_SCHEMA
|
step_id="reauth_confirm", data_schema=STEP_REAUTH_SCHEMA
|
||||||
)
|
)
|
||||||
|
|
||||||
assert self._reauth_entry
|
|
||||||
|
|
||||||
validation_result = await async_validate_credentials(
|
validation_result = await async_validate_credentials(
|
||||||
self._reauth_entry.data[CONF_IP_ADDRESS], user_input[CONF_PASSWORD]
|
self._reauth_entry_data[CONF_IP_ADDRESS], user_input[CONF_PASSWORD]
|
||||||
)
|
)
|
||||||
|
|
||||||
if validation_result.errors:
|
if validation_result.errors:
|
||||||
@ -115,13 +109,9 @@ class AirVisualProFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
errors=validation_result.errors,
|
errors=validation_result.errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.hass.config_entries.async_update_entry(
|
return self.async_update_reload_and_abort(
|
||||||
self._reauth_entry, data=self._reauth_entry.data | user_input
|
self._get_reauth_entry(), data_updates=user_input
|
||||||
)
|
)
|
||||||
self.hass.async_create_task(
|
|
||||||
self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
|
|
||||||
)
|
|
||||||
return self.async_abort(reason="reauth_successful")
|
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, str] | None = None
|
self, user_input: dict[str, str] | None = None
|
||||||
|
@ -24,6 +24,7 @@ PLATFORMS: list[Platform] = [
|
|||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
Platform.SELECT,
|
Platform.SELECT,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
|
Platform.SWITCH,
|
||||||
Platform.WATER_HEATER,
|
Platform.WATER_HEATER,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -85,6 +85,7 @@ HVAC_MODE_LIB_TO_HASS: Final[dict[OperationMode, HVACMode]] = {
|
|||||||
OperationMode.HEATING: HVACMode.HEAT,
|
OperationMode.HEATING: HVACMode.HEAT,
|
||||||
OperationMode.FAN: HVACMode.FAN_ONLY,
|
OperationMode.FAN: HVACMode.FAN_ONLY,
|
||||||
OperationMode.DRY: HVACMode.DRY,
|
OperationMode.DRY: HVACMode.DRY,
|
||||||
|
OperationMode.AUX_HEATING: HVACMode.HEAT,
|
||||||
OperationMode.AUTO: HVACMode.HEAT_COOL,
|
OperationMode.AUTO: HVACMode.HEAT_COOL,
|
||||||
}
|
}
|
||||||
HVAC_MODE_HASS_TO_LIB: Final[dict[HVACMode, OperationMode]] = {
|
HVAC_MODE_HASS_TO_LIB: Final[dict[HVACMode, OperationMode]] = {
|
||||||
@ -157,9 +158,10 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
|||||||
self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[
|
self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[
|
||||||
self.get_airzone_value(AZD_TEMP_UNIT)
|
self.get_airzone_value(AZD_TEMP_UNIT)
|
||||||
]
|
]
|
||||||
self._attr_hvac_modes = [
|
_attr_hvac_modes = [
|
||||||
HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES)
|
HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES)
|
||||||
]
|
]
|
||||||
|
self._attr_hvac_modes = list(dict.fromkeys(_attr_hvac_modes))
|
||||||
if (
|
if (
|
||||||
self.get_airzone_value(AZD_SPEED) is not None
|
self.get_airzone_value(AZD_SPEED) is not None
|
||||||
and self.get_airzone_value(AZD_SPEEDS) is not None
|
and self.get_airzone_value(AZD_SPEEDS) is not None
|
||||||
@ -273,12 +275,18 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
|||||||
self._attr_min_temp = self.get_airzone_value(AZD_TEMP_MIN)
|
self._attr_min_temp = self.get_airzone_value(AZD_TEMP_MIN)
|
||||||
if self.supported_features & ClimateEntityFeature.FAN_MODE:
|
if self.supported_features & ClimateEntityFeature.FAN_MODE:
|
||||||
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
|
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
|
||||||
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
if (
|
||||||
|
self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||||
|
and self._attr_hvac_mode == HVACMode.HEAT_COOL
|
||||||
|
):
|
||||||
self._attr_target_temperature_high = self.get_airzone_value(
|
self._attr_target_temperature_high = self.get_airzone_value(
|
||||||
AZD_COOL_TEMP_SET
|
AZD_COOL_TEMP_SET
|
||||||
)
|
)
|
||||||
self._attr_target_temperature_low = self.get_airzone_value(
|
self._attr_target_temperature_low = self.get_airzone_value(
|
||||||
AZD_HEAT_TEMP_SET
|
AZD_HEAT_TEMP_SET
|
||||||
)
|
)
|
||||||
|
self._attr_target_temperature = None
|
||||||
else:
|
else:
|
||||||
|
self._attr_target_temperature_high = None
|
||||||
|
self._attr_target_temperature_low = None
|
||||||
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)
|
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)
|
||||||
|
@ -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.3"]
|
"requirements": ["aioairzone==0.9.5"]
|
||||||
}
|
}
|
||||||
|
122
homeassistant/components/airzone/switch.py
Normal file
122
homeassistant/components/airzone/switch.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
"""Support for the Airzone switch."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
from aioairzone.const import API_ON, AZD_ON, AZD_ZONES
|
||||||
|
|
||||||
|
from homeassistant.components.switch import (
|
||||||
|
SwitchDeviceClass,
|
||||||
|
SwitchEntity,
|
||||||
|
SwitchEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AirzoneConfigEntry
|
||||||
|
from .coordinator import AirzoneUpdateCoordinator
|
||||||
|
from .entity import AirzoneEntity, AirzoneZoneEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AirzoneSwitchDescription(SwitchEntityDescription):
|
||||||
|
"""Class to describe an Airzone switch entity."""
|
||||||
|
|
||||||
|
api_param: str
|
||||||
|
|
||||||
|
|
||||||
|
ZONE_SWITCH_TYPES: Final[tuple[AirzoneSwitchDescription, ...]] = (
|
||||||
|
AirzoneSwitchDescription(
|
||||||
|
api_param=API_ON,
|
||||||
|
device_class=SwitchDeviceClass.SWITCH,
|
||||||
|
key=AZD_ON,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AirzoneConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Add Airzone switch from a config_entry."""
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
|
added_zones: set[str] = set()
|
||||||
|
|
||||||
|
def _async_entity_listener() -> None:
|
||||||
|
"""Handle additions of switch."""
|
||||||
|
|
||||||
|
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||||
|
received_zones = set(zones_data)
|
||||||
|
new_zones = received_zones - added_zones
|
||||||
|
if new_zones:
|
||||||
|
async_add_entities(
|
||||||
|
AirzoneZoneSwitch(
|
||||||
|
coordinator,
|
||||||
|
description,
|
||||||
|
entry,
|
||||||
|
system_zone_id,
|
||||||
|
zones_data.get(system_zone_id),
|
||||||
|
)
|
||||||
|
for system_zone_id in new_zones
|
||||||
|
for description in ZONE_SWITCH_TYPES
|
||||||
|
if description.key in zones_data.get(system_zone_id)
|
||||||
|
)
|
||||||
|
added_zones.update(new_zones)
|
||||||
|
|
||||||
|
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||||
|
_async_entity_listener()
|
||||||
|
|
||||||
|
|
||||||
|
class AirzoneBaseSwitch(AirzoneEntity, SwitchEntity):
|
||||||
|
"""Define an Airzone switch."""
|
||||||
|
|
||||||
|
entity_description: AirzoneSwitchDescription
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Update attributes when the coordinator updates."""
|
||||||
|
self._async_update_attrs()
|
||||||
|
super()._handle_coordinator_update()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update switch attributes."""
|
||||||
|
self._attr_is_on = self.get_airzone_value(self.entity_description.key)
|
||||||
|
|
||||||
|
|
||||||
|
class AirzoneZoneSwitch(AirzoneZoneEntity, AirzoneBaseSwitch):
|
||||||
|
"""Define an Airzone Zone switch."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirzoneUpdateCoordinator,
|
||||||
|
description: AirzoneSwitchDescription,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
system_zone_id: str,
|
||||||
|
zone_data: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
super().__init__(coordinator, entry, system_zone_id, zone_data)
|
||||||
|
|
||||||
|
self._attr_name = None
|
||||||
|
self._attr_unique_id = (
|
||||||
|
f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
|
||||||
|
)
|
||||||
|
self.entity_description = description
|
||||||
|
|
||||||
|
self._async_update_attrs()
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the entity on."""
|
||||||
|
param = self.entity_description.api_param
|
||||||
|
await self._async_update_hvac_params({param: True})
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the entity off."""
|
||||||
|
param = self.entity_description.api_param
|
||||||
|
await self._async_update_hvac_params({param: False})
|
@ -17,6 +17,7 @@ PLATFORMS: list[Platform] = [
|
|||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
Platform.SELECT,
|
Platform.SELECT,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
|
Platform.SWITCH,
|
||||||
Platform.WATER_HEATER,
|
Platform.WATER_HEATER,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -224,14 +224,20 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity):
|
|||||||
self._attr_hvac_mode = HVACMode.OFF
|
self._attr_hvac_mode = HVACMode.OFF
|
||||||
self._attr_max_temp = self.get_airzone_value(AZD_TEMP_SET_MAX)
|
self._attr_max_temp = self.get_airzone_value(AZD_TEMP_SET_MAX)
|
||||||
self._attr_min_temp = self.get_airzone_value(AZD_TEMP_SET_MIN)
|
self._attr_min_temp = self.get_airzone_value(AZD_TEMP_SET_MIN)
|
||||||
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
if (
|
||||||
|
self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||||
|
and self._attr_hvac_mode == HVACMode.HEAT_COOL
|
||||||
|
):
|
||||||
self._attr_target_temperature_high = self.get_airzone_value(
|
self._attr_target_temperature_high = self.get_airzone_value(
|
||||||
AZD_TEMP_SET_COOL_AIR
|
AZD_TEMP_SET_COOL_AIR
|
||||||
)
|
)
|
||||||
self._attr_target_temperature_low = self.get_airzone_value(
|
self._attr_target_temperature_low = self.get_airzone_value(
|
||||||
AZD_TEMP_SET_HOT_AIR
|
AZD_TEMP_SET_HOT_AIR
|
||||||
)
|
)
|
||||||
|
self._attr_target_temperature = None
|
||||||
else:
|
else:
|
||||||
|
self._attr_target_temperature_high = None
|
||||||
|
self._attr_target_temperature_low = None
|
||||||
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)
|
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)
|
||||||
|
|
||||||
|
|
||||||
@ -304,6 +310,10 @@ class AirzoneDeviceClimate(AirzoneClimate):
|
|||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
|
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
||||||
|
if hvac_mode is not None:
|
||||||
|
await self.async_set_hvac_mode(hvac_mode)
|
||||||
|
|
||||||
params: dict[str, Any] = {}
|
params: dict[str, Any] = {}
|
||||||
if ATTR_TEMPERATURE in kwargs:
|
if ATTR_TEMPERATURE in kwargs:
|
||||||
params[API_SETPOINT] = {
|
params[API_SETPOINT] = {
|
||||||
@ -327,9 +337,6 @@ class AirzoneDeviceClimate(AirzoneClimate):
|
|||||||
}
|
}
|
||||||
await self._async_update_params(params)
|
await self._async_update_params(params)
|
||||||
|
|
||||||
if ATTR_HVAC_MODE in kwargs:
|
|
||||||
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
|
|
||||||
|
|
||||||
|
|
||||||
class AirzoneDeviceGroupClimate(AirzoneClimate):
|
class AirzoneDeviceGroupClimate(AirzoneClimate):
|
||||||
"""Define an Airzone Cloud DeviceGroup base class."""
|
"""Define an Airzone Cloud DeviceGroup base class."""
|
||||||
@ -360,6 +367,10 @@ class AirzoneDeviceGroupClimate(AirzoneClimate):
|
|||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
|
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
||||||
|
if hvac_mode is not None:
|
||||||
|
await self.async_set_hvac_mode(hvac_mode)
|
||||||
|
|
||||||
params: dict[str, Any] = {}
|
params: dict[str, Any] = {}
|
||||||
if ATTR_TEMPERATURE in kwargs:
|
if ATTR_TEMPERATURE in kwargs:
|
||||||
params[API_PARAMS] = {
|
params[API_PARAMS] = {
|
||||||
@ -370,9 +381,6 @@ class AirzoneDeviceGroupClimate(AirzoneClimate):
|
|||||||
}
|
}
|
||||||
await self._async_update_params(params)
|
await self._async_update_params(params)
|
||||||
|
|
||||||
if ATTR_HVAC_MODE in kwargs:
|
|
||||||
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
|
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
"""Set hvac mode."""
|
"""Set hvac mode."""
|
||||||
params: dict[str, Any] = {
|
params: dict[str, Any] = {
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioairzone_cloud"],
|
"loggers": ["aioairzone_cloud"],
|
||||||
"requirements": ["aioairzone-cloud==0.6.5"]
|
"requirements": ["aioairzone-cloud==0.6.10"]
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,19 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
from aioairzone_cloud.common import AirQualityMode
|
from aioairzone_cloud.common import AirQualityMode, OperationMode
|
||||||
from aioairzone_cloud.const import (
|
from aioairzone_cloud.const import (
|
||||||
API_AQ_MODE_CONF,
|
API_AQ_MODE_CONF,
|
||||||
|
API_MODE,
|
||||||
API_VALUE,
|
API_VALUE,
|
||||||
AZD_AQ_MODE_CONF,
|
AZD_AQ_MODE_CONF,
|
||||||
|
AZD_MASTER,
|
||||||
|
AZD_MODE,
|
||||||
|
AZD_MODES,
|
||||||
AZD_ZONES,
|
AZD_ZONES,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,7 +33,10 @@ class AirzoneSelectDescription(SelectEntityDescription):
|
|||||||
"""Class to describe an Airzone select entity."""
|
"""Class to describe an Airzone select entity."""
|
||||||
|
|
||||||
api_param: str
|
api_param: str
|
||||||
options_dict: dict[str, str]
|
options_dict: dict[str, Any]
|
||||||
|
options_fn: Callable[[dict[str, Any], dict[str, Any]], list[str]] = (
|
||||||
|
lambda zone_data, value: list(value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
AIR_QUALITY_MAP: Final[dict[str, str]] = {
|
AIR_QUALITY_MAP: Final[dict[str, str]] = {
|
||||||
@ -37,6 +45,35 @@ AIR_QUALITY_MAP: Final[dict[str, str]] = {
|
|||||||
"auto": AirQualityMode.AUTO,
|
"auto": AirQualityMode.AUTO,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MODE_MAP: Final[dict[str, int]] = {
|
||||||
|
"cool": OperationMode.COOLING,
|
||||||
|
"dry": OperationMode.DRY,
|
||||||
|
"fan": OperationMode.VENTILATION,
|
||||||
|
"heat": OperationMode.HEATING,
|
||||||
|
"heat_cool": OperationMode.AUTO,
|
||||||
|
"stop": OperationMode.STOP,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main_zone_options(
|
||||||
|
zone_data: dict[str, Any],
|
||||||
|
options: dict[str, int],
|
||||||
|
) -> list[str]:
|
||||||
|
"""Filter available modes."""
|
||||||
|
modes = zone_data.get(AZD_MODES, [])
|
||||||
|
return [k for k, v in options.items() if v in modes]
|
||||||
|
|
||||||
|
|
||||||
|
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||||
|
AirzoneSelectDescription(
|
||||||
|
api_param=API_MODE,
|
||||||
|
key=AZD_MODE,
|
||||||
|
options_dict=MODE_MAP,
|
||||||
|
options_fn=main_zone_options,
|
||||||
|
translation_key="modes",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||||
AirzoneSelectDescription(
|
AirzoneSelectDescription(
|
||||||
@ -59,7 +96,19 @@ async def async_setup_entry(
|
|||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
# Zones
|
# Zones
|
||||||
async_add_entities(
|
entities: list[AirzoneZoneSelect] = [
|
||||||
|
AirzoneZoneSelect(
|
||||||
|
coordinator,
|
||||||
|
description,
|
||||||
|
zone_id,
|
||||||
|
zone_data,
|
||||||
|
)
|
||||||
|
for description in MAIN_ZONE_SELECT_TYPES
|
||||||
|
for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items()
|
||||||
|
if description.key in zone_data and zone_data.get(AZD_MASTER)
|
||||||
|
]
|
||||||
|
|
||||||
|
entities.extend(
|
||||||
AirzoneZoneSelect(
|
AirzoneZoneSelect(
|
||||||
coordinator,
|
coordinator,
|
||||||
description,
|
description,
|
||||||
@ -71,6 +120,8 @@ async def async_setup_entry(
|
|||||||
if description.key in zone_data
|
if description.key in zone_data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
|
class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
|
||||||
"""Define an Airzone Cloud select."""
|
"""Define an Airzone Cloud select."""
|
||||||
@ -110,6 +161,11 @@ class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
|
|||||||
|
|
||||||
self._attr_unique_id = f"{zone_id}_{description.key}"
|
self._attr_unique_id = f"{zone_id}_{description.key}"
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
|
self._attr_options = self.entity_description.options_fn(
|
||||||
|
zone_data, description.options_dict
|
||||||
|
)
|
||||||
|
|
||||||
self.values_dict = {v: k for k, v in description.options_dict.items()}
|
self.values_dict = {v: k for k, v in description.options_dict.items()}
|
||||||
|
|
||||||
self._async_update_attrs()
|
self._async_update_attrs()
|
||||||
|
@ -36,6 +36,17 @@
|
|||||||
"on": "On",
|
"on": "On",
|
||||||
"auto": "Auto"
|
"auto": "Auto"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"name": "Mode",
|
||||||
|
"state": {
|
||||||
|
"cool": "[%key:component::climate::entity_component::_::state::cool%]",
|
||||||
|
"dry": "[%key:component::climate::entity_component::_::state::dry%]",
|
||||||
|
"fan": "[%key:component::climate::entity_component::_::state::fan_only%]",
|
||||||
|
"heat": "[%key:component::climate::entity_component::_::state::heat%]",
|
||||||
|
"heat_cool": "[%key:component::climate::entity_component::_::state::heat_cool%]",
|
||||||
|
"stop": "Stop"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
115
homeassistant/components/airzone_cloud/switch.py
Normal file
115
homeassistant/components/airzone_cloud/switch.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
"""Support for the Airzone Cloud switch."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
from aioairzone_cloud.const import API_POWER, API_VALUE, AZD_POWER, AZD_ZONES
|
||||||
|
|
||||||
|
from homeassistant.components.switch import (
|
||||||
|
SwitchDeviceClass,
|
||||||
|
SwitchEntity,
|
||||||
|
SwitchEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AirzoneCloudConfigEntry
|
||||||
|
from .coordinator import AirzoneUpdateCoordinator
|
||||||
|
from .entity import AirzoneEntity, AirzoneZoneEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AirzoneSwitchDescription(SwitchEntityDescription):
|
||||||
|
"""Class to describe an Airzone switch entity."""
|
||||||
|
|
||||||
|
api_param: str
|
||||||
|
|
||||||
|
|
||||||
|
ZONE_SWITCH_TYPES: Final[tuple[AirzoneSwitchDescription, ...]] = (
|
||||||
|
AirzoneSwitchDescription(
|
||||||
|
api_param=API_POWER,
|
||||||
|
device_class=SwitchDeviceClass.SWITCH,
|
||||||
|
key=AZD_POWER,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AirzoneCloudConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Add Airzone Cloud switch from a config_entry."""
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
|
# Zones
|
||||||
|
async_add_entities(
|
||||||
|
AirzoneZoneSwitch(
|
||||||
|
coordinator,
|
||||||
|
description,
|
||||||
|
zone_id,
|
||||||
|
zone_data,
|
||||||
|
)
|
||||||
|
for description in ZONE_SWITCH_TYPES
|
||||||
|
for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items()
|
||||||
|
if description.key in zone_data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AirzoneBaseSwitch(AirzoneEntity, SwitchEntity):
|
||||||
|
"""Define an Airzone Cloud switch."""
|
||||||
|
|
||||||
|
entity_description: AirzoneSwitchDescription
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Update attributes when the coordinator updates."""
|
||||||
|
self._async_update_attrs()
|
||||||
|
super()._handle_coordinator_update()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update switch attributes."""
|
||||||
|
self._attr_is_on = self.get_airzone_value(self.entity_description.key)
|
||||||
|
|
||||||
|
|
||||||
|
class AirzoneZoneSwitch(AirzoneZoneEntity, AirzoneBaseSwitch):
|
||||||
|
"""Define an Airzone Cloud Zone switch."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirzoneUpdateCoordinator,
|
||||||
|
description: AirzoneSwitchDescription,
|
||||||
|
zone_id: str,
|
||||||
|
zone_data: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
super().__init__(coordinator, zone_id, zone_data)
|
||||||
|
|
||||||
|
self._attr_name = None
|
||||||
|
self._attr_unique_id = f"{zone_id}_{description.key}"
|
||||||
|
self.entity_description = description
|
||||||
|
|
||||||
|
self._async_update_attrs()
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the entity on."""
|
||||||
|
param = self.entity_description.api_param
|
||||||
|
params: dict[str, Any] = {
|
||||||
|
param: {
|
||||||
|
API_VALUE: True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await self._async_update_params(params)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the entity off."""
|
||||||
|
param = self.entity_description.api_param
|
||||||
|
params: dict[str, Any] = {
|
||||||
|
param: {
|
||||||
|
API_VALUE: False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await self._async_update_params(params)
|
@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import cached_property, partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Final, final
|
from typing import Any, Final, final
|
||||||
|
|
||||||
|
from propcache import cached_property
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -32,6 +34,7 @@ from homeassistant.helpers.deprecation import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
@ -48,12 +51,13 @@ from .const import ( # noqa: F401
|
|||||||
ATTR_CODE_ARM_REQUIRED,
|
ATTR_CODE_ARM_REQUIRED,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
|
AlarmControlPanelState,
|
||||||
CodeFormat,
|
CodeFormat,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN_DATA: HassKey[EntityComponent[AlarmControlPanelEntity]] = HassKey(DOMAIN)
|
DATA_COMPONENT: HassKey[EntityComponent[AlarmControlPanelEntity]] = HassKey(DOMAIN)
|
||||||
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
||||||
PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA
|
PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA
|
||||||
PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE
|
PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE
|
||||||
@ -71,7 +75,7 @@ ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema(
|
|||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Track states and offer events for sensors."""
|
"""Track states and offer events for sensors."""
|
||||||
component = hass.data[DOMAIN_DATA] = EntityComponent[AlarmControlPanelEntity](
|
component = hass.data[DATA_COMPONENT] = EntityComponent[AlarmControlPanelEntity](
|
||||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -124,12 +128,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up a config entry."""
|
"""Set up a config entry."""
|
||||||
return await hass.data[DOMAIN_DATA].async_setup_entry(entry)
|
return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
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.data[DOMAIN_DATA].async_unload_entry(entry)
|
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
class AlarmControlPanelEntityDescription(EntityDescription, frozen_or_thawed=True):
|
class AlarmControlPanelEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||||
@ -141,6 +145,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
|||||||
"changed_by",
|
"changed_by",
|
||||||
"code_arm_required",
|
"code_arm_required",
|
||||||
"supported_features",
|
"supported_features",
|
||||||
|
"alarm_state",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -148,6 +153,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
|||||||
"""An abstract class for alarm control entities."""
|
"""An abstract class for alarm control entities."""
|
||||||
|
|
||||||
entity_description: AlarmControlPanelEntityDescription
|
entity_description: AlarmControlPanelEntityDescription
|
||||||
|
_attr_alarm_state: AlarmControlPanelState | None = None
|
||||||
_attr_changed_by: str | None = None
|
_attr_changed_by: str | None = None
|
||||||
_attr_code_arm_required: bool = True
|
_attr_code_arm_required: bool = True
|
||||||
_attr_code_format: CodeFormat | None = None
|
_attr_code_format: CodeFormat | None = None
|
||||||
@ -156,6 +162,78 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
|||||||
)
|
)
|
||||||
_alarm_control_panel_option_default_code: str | None = None
|
_alarm_control_panel_option_default_code: str | None = None
|
||||||
|
|
||||||
|
__alarm_legacy_state: bool = False
|
||||||
|
__alarm_legacy_state_reported: bool = False
|
||||||
|
|
||||||
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||||
|
"""Post initialisation processing."""
|
||||||
|
super().__init_subclass__(**kwargs)
|
||||||
|
if any(method in cls.__dict__ for method in ("_attr_state", "state")):
|
||||||
|
# Integrations should use the 'alarm_state' property instead of
|
||||||
|
# setting the state directly.
|
||||||
|
cls.__alarm_legacy_state = True
|
||||||
|
|
||||||
|
def __setattr__(self, __name: str, __value: Any) -> None:
|
||||||
|
"""Set attribute.
|
||||||
|
|
||||||
|
Deprecation warning if setting '_attr_state' directly
|
||||||
|
unless already reported.
|
||||||
|
"""
|
||||||
|
if __name == "_attr_state":
|
||||||
|
if self.__alarm_legacy_state_reported is not True:
|
||||||
|
self._report_deprecated_alarm_state_handling()
|
||||||
|
self.__alarm_legacy_state_reported = True
|
||||||
|
return super().__setattr__(__name, __value)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def add_to_platform_start(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
platform: EntityPlatform,
|
||||||
|
parallel_updates: asyncio.Semaphore | None,
|
||||||
|
) -> None:
|
||||||
|
"""Start adding an entity to a platform."""
|
||||||
|
super().add_to_platform_start(hass, platform, parallel_updates)
|
||||||
|
if self.__alarm_legacy_state and not self.__alarm_legacy_state_reported:
|
||||||
|
self._report_deprecated_alarm_state_handling()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _report_deprecated_alarm_state_handling(self) -> None:
|
||||||
|
"""Report on deprecated handling of alarm state.
|
||||||
|
|
||||||
|
Integrations should implement alarm_state instead of using state directly.
|
||||||
|
"""
|
||||||
|
self.__alarm_legacy_state_reported = True
|
||||||
|
if "custom_components" in type(self).__module__:
|
||||||
|
# Do not report on core integrations as they have been fixed.
|
||||||
|
report_issue = "report it to the custom integration author."
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Entity %s (%s) is setting state directly"
|
||||||
|
" which will stop working in HA Core 2025.11."
|
||||||
|
" Entities should implement the 'alarm_state' property and"
|
||||||
|
" return its state using the AlarmControlPanelState enum, please %s",
|
||||||
|
self.entity_id,
|
||||||
|
type(self),
|
||||||
|
report_issue,
|
||||||
|
)
|
||||||
|
|
||||||
|
@final
|
||||||
|
@property
|
||||||
|
def state(self) -> str | None:
|
||||||
|
"""Return the current state."""
|
||||||
|
if (alarm_state := self.alarm_state) is None:
|
||||||
|
return None
|
||||||
|
return alarm_state
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def alarm_state(self) -> AlarmControlPanelState | None:
|
||||||
|
"""Return the current alarm control panel entity state.
|
||||||
|
|
||||||
|
Integrations should overwrite this or use the '_attr_alarm_state'
|
||||||
|
attribute to set the alarm status using the 'AlarmControlPanelState' enum.
|
||||||
|
"""
|
||||||
|
return self._attr_alarm_state
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@callback
|
@callback
|
||||||
def code_or_default_code(self, code: str | None) -> str | None:
|
def code_or_default_code(self, code: str | None) -> str | None:
|
||||||
|
@ -17,6 +17,21 @@ ATTR_CHANGED_BY: Final = "changed_by"
|
|||||||
ATTR_CODE_ARM_REQUIRED: Final = "code_arm_required"
|
ATTR_CODE_ARM_REQUIRED: Final = "code_arm_required"
|
||||||
|
|
||||||
|
|
||||||
|
class AlarmControlPanelState(StrEnum):
|
||||||
|
"""Alarm control panel entity states."""
|
||||||
|
|
||||||
|
DISARMED = "disarmed"
|
||||||
|
ARMED_HOME = "armed_home"
|
||||||
|
ARMED_AWAY = "armed_away"
|
||||||
|
ARMED_NIGHT = "armed_night"
|
||||||
|
ARMED_VACATION = "armed_vacation"
|
||||||
|
ARMED_CUSTOM_BYPASS = "armed_custom_bypass"
|
||||||
|
PENDING = "pending"
|
||||||
|
ARMING = "arming"
|
||||||
|
DISARMING = "disarming"
|
||||||
|
TRIGGERED = "triggered"
|
||||||
|
|
||||||
|
|
||||||
class CodeFormat(StrEnum):
|
class CodeFormat(StrEnum):
|
||||||
"""Code formats for the Alarm Control Panel."""
|
"""Code formats for the Alarm Control Panel."""
|
||||||
|
|
||||||
|
@ -13,13 +13,6 @@ from homeassistant.const import (
|
|||||||
CONF_DOMAIN,
|
CONF_DOMAIN,
|
||||||
CONF_ENTITY_ID,
|
CONF_ENTITY_ID,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
|
||||||
STATE_ALARM_ARMED_VACATION,
|
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
STATE_ALARM_TRIGGERED,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
@ -31,7 +24,7 @@ from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
|
|||||||
from homeassistant.helpers.entity import get_supported_features
|
from homeassistant.helpers.entity import get_supported_features
|
||||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||||
|
|
||||||
from . import DOMAIN
|
from . import DOMAIN, AlarmControlPanelState
|
||||||
from .const import (
|
from .const import (
|
||||||
CONDITION_ARMED_AWAY,
|
CONDITION_ARMED_AWAY,
|
||||||
CONDITION_ARMED_CUSTOM_BYPASS,
|
CONDITION_ARMED_CUSTOM_BYPASS,
|
||||||
@ -109,19 +102,19 @@ def async_condition_from_config(
|
|||||||
) -> condition.ConditionCheckerType:
|
) -> condition.ConditionCheckerType:
|
||||||
"""Create a function to test a device condition."""
|
"""Create a function to test a device condition."""
|
||||||
if config[CONF_TYPE] == CONDITION_TRIGGERED:
|
if config[CONF_TYPE] == CONDITION_TRIGGERED:
|
||||||
state = STATE_ALARM_TRIGGERED
|
state = AlarmControlPanelState.TRIGGERED
|
||||||
elif config[CONF_TYPE] == CONDITION_DISARMED:
|
elif config[CONF_TYPE] == CONDITION_DISARMED:
|
||||||
state = STATE_ALARM_DISARMED
|
state = AlarmControlPanelState.DISARMED
|
||||||
elif config[CONF_TYPE] == CONDITION_ARMED_HOME:
|
elif config[CONF_TYPE] == CONDITION_ARMED_HOME:
|
||||||
state = STATE_ALARM_ARMED_HOME
|
state = AlarmControlPanelState.ARMED_HOME
|
||||||
elif config[CONF_TYPE] == CONDITION_ARMED_AWAY:
|
elif config[CONF_TYPE] == CONDITION_ARMED_AWAY:
|
||||||
state = STATE_ALARM_ARMED_AWAY
|
state = AlarmControlPanelState.ARMED_AWAY
|
||||||
elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT:
|
elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT:
|
||||||
state = STATE_ALARM_ARMED_NIGHT
|
state = AlarmControlPanelState.ARMED_NIGHT
|
||||||
elif config[CONF_TYPE] == CONDITION_ARMED_VACATION:
|
elif config[CONF_TYPE] == CONDITION_ARMED_VACATION:
|
||||||
state = STATE_ALARM_ARMED_VACATION
|
state = AlarmControlPanelState.ARMED_VACATION
|
||||||
elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS:
|
elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS:
|
||||||
state = STATE_ALARM_ARMED_CUSTOM_BYPASS
|
state = AlarmControlPanelState.ARMED_CUSTOM_BYPASS
|
||||||
|
|
||||||
registry = er.async_get(hass)
|
registry = er.async_get(hass)
|
||||||
entity_id = er.async_resolve_entity_id(registry, config[ATTR_ENTITY_ID])
|
entity_id = er.async_resolve_entity_id(registry, config[ATTR_ENTITY_ID])
|
||||||
|
@ -15,13 +15,6 @@ from homeassistant.const import (
|
|||||||
CONF_FOR,
|
CONF_FOR,
|
||||||
CONF_PLATFORM,
|
CONF_PLATFORM,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
|
||||||
STATE_ALARM_ARMED_VACATION,
|
|
||||||
STATE_ALARM_ARMING,
|
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
STATE_ALARM_TRIGGERED,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||||
@ -29,7 +22,7 @@ from homeassistant.helpers.entity import get_supported_features
|
|||||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from . import DOMAIN
|
from . import DOMAIN, AlarmControlPanelState
|
||||||
from .const import AlarmControlPanelEntityFeature
|
from .const import AlarmControlPanelEntityFeature
|
||||||
|
|
||||||
BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"}
|
BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"}
|
||||||
@ -129,19 +122,19 @@ async def async_attach_trigger(
|
|||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Attach a trigger."""
|
"""Attach a trigger."""
|
||||||
if config[CONF_TYPE] == "triggered":
|
if config[CONF_TYPE] == "triggered":
|
||||||
to_state = STATE_ALARM_TRIGGERED
|
to_state = AlarmControlPanelState.TRIGGERED
|
||||||
elif config[CONF_TYPE] == "disarmed":
|
elif config[CONF_TYPE] == "disarmed":
|
||||||
to_state = STATE_ALARM_DISARMED
|
to_state = AlarmControlPanelState.DISARMED
|
||||||
elif config[CONF_TYPE] == "arming":
|
elif config[CONF_TYPE] == "arming":
|
||||||
to_state = STATE_ALARM_ARMING
|
to_state = AlarmControlPanelState.ARMING
|
||||||
elif config[CONF_TYPE] == "armed_home":
|
elif config[CONF_TYPE] == "armed_home":
|
||||||
to_state = STATE_ALARM_ARMED_HOME
|
to_state = AlarmControlPanelState.ARMED_HOME
|
||||||
elif config[CONF_TYPE] == "armed_away":
|
elif config[CONF_TYPE] == "armed_away":
|
||||||
to_state = STATE_ALARM_ARMED_AWAY
|
to_state = AlarmControlPanelState.ARMED_AWAY
|
||||||
elif config[CONF_TYPE] == "armed_night":
|
elif config[CONF_TYPE] == "armed_night":
|
||||||
to_state = STATE_ALARM_ARMED_NIGHT
|
to_state = AlarmControlPanelState.ARMED_NIGHT
|
||||||
elif config[CONF_TYPE] == "armed_vacation":
|
elif config[CONF_TYPE] == "armed_vacation":
|
||||||
to_state = STATE_ALARM_ARMED_VACATION
|
to_state = AlarmControlPanelState.ARMED_VACATION
|
||||||
|
|
||||||
state_config = {
|
state_config = {
|
||||||
state_trigger.CONF_PLATFORM: "state",
|
state_trigger.CONF_PLATFORM: "state",
|
||||||
|
@ -16,28 +16,21 @@ from homeassistant.const import (
|
|||||||
SERVICE_ALARM_ARM_VACATION,
|
SERVICE_ALARM_ARM_VACATION,
|
||||||
SERVICE_ALARM_DISARM,
|
SERVICE_ALARM_DISARM,
|
||||||
SERVICE_ALARM_TRIGGER,
|
SERVICE_ALARM_TRIGGER,
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
|
||||||
STATE_ALARM_ARMED_VACATION,
|
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
STATE_ALARM_TRIGGERED,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import Context, HomeAssistant, State
|
from homeassistant.core import Context, HomeAssistant, State
|
||||||
|
|
||||||
from . import DOMAIN
|
from . import DOMAIN, AlarmControlPanelState
|
||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
|
||||||
VALID_STATES: Final[set[str]] = {
|
VALID_STATES: Final[set[str]] = {
|
||||||
STATE_ALARM_ARMED_AWAY,
|
AlarmControlPanelState.ARMED_AWAY,
|
||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
|
||||||
STATE_ALARM_ARMED_HOME,
|
AlarmControlPanelState.ARMED_HOME,
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
AlarmControlPanelState.ARMED_NIGHT,
|
||||||
STATE_ALARM_ARMED_VACATION,
|
AlarmControlPanelState.ARMED_VACATION,
|
||||||
STATE_ALARM_DISARMED,
|
AlarmControlPanelState.DISARMED,
|
||||||
STATE_ALARM_TRIGGERED,
|
AlarmControlPanelState.TRIGGERED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -65,19 +58,19 @@ async def _async_reproduce_state(
|
|||||||
|
|
||||||
service_data = {ATTR_ENTITY_ID: state.entity_id}
|
service_data = {ATTR_ENTITY_ID: state.entity_id}
|
||||||
|
|
||||||
if state.state == STATE_ALARM_ARMED_AWAY:
|
if state.state == AlarmControlPanelState.ARMED_AWAY:
|
||||||
service = SERVICE_ALARM_ARM_AWAY
|
service = SERVICE_ALARM_ARM_AWAY
|
||||||
elif state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
|
elif state.state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS:
|
||||||
service = SERVICE_ALARM_ARM_CUSTOM_BYPASS
|
service = SERVICE_ALARM_ARM_CUSTOM_BYPASS
|
||||||
elif state.state == STATE_ALARM_ARMED_HOME:
|
elif state.state == AlarmControlPanelState.ARMED_HOME:
|
||||||
service = SERVICE_ALARM_ARM_HOME
|
service = SERVICE_ALARM_ARM_HOME
|
||||||
elif state.state == STATE_ALARM_ARMED_NIGHT:
|
elif state.state == AlarmControlPanelState.ARMED_NIGHT:
|
||||||
service = SERVICE_ALARM_ARM_NIGHT
|
service = SERVICE_ALARM_ARM_NIGHT
|
||||||
elif state.state == STATE_ALARM_ARMED_VACATION:
|
elif state.state == AlarmControlPanelState.ARMED_VACATION:
|
||||||
service = SERVICE_ALARM_ARM_VACATION
|
service = SERVICE_ALARM_ARM_VACATION
|
||||||
elif state.state == STATE_ALARM_DISARMED:
|
elif state.state == AlarmControlPanelState.DISARMED:
|
||||||
service = SERVICE_ALARM_DISARM
|
service = SERVICE_ALARM_DISARM
|
||||||
elif state.state == STATE_ALARM_TRIGGERED:
|
elif state.state == AlarmControlPanelState.TRIGGERED:
|
||||||
service = SERVICE_ALARM_TRIGGER
|
service = SERVICE_ALARM_TRIGGER
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
|
@ -7,16 +7,10 @@ import voluptuous as vol
|
|||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
AlarmControlPanelEntity,
|
AlarmControlPanelEntity,
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
|
AlarmControlPanelState,
|
||||||
CodeFormat,
|
CodeFormat,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import ATTR_CODE
|
||||||
ATTR_CODE,
|
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
STATE_ALARM_TRIGGERED,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_platform
|
from homeassistant.helpers import entity_platform
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -106,15 +100,15 @@ class AlarmDecoderAlarmPanel(AlarmDecoderEntity, AlarmControlPanelEntity):
|
|||||||
def _message_callback(self, message):
|
def _message_callback(self, message):
|
||||||
"""Handle received messages."""
|
"""Handle received messages."""
|
||||||
if message.alarm_sounding or message.fire_alarm:
|
if message.alarm_sounding or message.fire_alarm:
|
||||||
self._attr_state = STATE_ALARM_TRIGGERED
|
self._attr_alarm_state = AlarmControlPanelState.TRIGGERED
|
||||||
elif message.armed_away:
|
elif message.armed_away:
|
||||||
self._attr_state = STATE_ALARM_ARMED_AWAY
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_AWAY
|
||||||
elif message.armed_home and (message.entry_delay_off or message.perimeter_only):
|
elif message.armed_home and (message.entry_delay_off or message.perimeter_only):
|
||||||
self._attr_state = STATE_ALARM_ARMED_NIGHT
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_NIGHT
|
||||||
elif message.armed_home:
|
elif message.armed_home:
|
||||||
self._attr_state = STATE_ALARM_ARMED_HOME
|
self._attr_alarm_state = AlarmControlPanelState.ARMED_HOME
|
||||||
else:
|
else:
|
||||||
self._attr_state = STATE_ALARM_DISARMED
|
self._attr_alarm_state = AlarmControlPanelState.DISARMED
|
||||||
|
|
||||||
self._attr_extra_state_attributes = {
|
self._attr_extra_state_attributes = {
|
||||||
"ac_power": message.ac_power,
|
"ac_power": message.ac_power,
|
||||||
|
@ -157,7 +157,7 @@ class AlarmDecoderFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
class AlarmDecoderOptionsFlowHandler(OptionsFlow):
|
class AlarmDecoderOptionsFlowHandler(OptionsFlow):
|
||||||
"""Handle AlarmDecoder options."""
|
"""Handle AlarmDecoder options."""
|
||||||
|
|
||||||
selected_zone: str | None = None
|
selected_zone: str
|
||||||
|
|
||||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||||
"""Initialize AlarmDecoder options flow."""
|
"""Initialize AlarmDecoder options flow."""
|
||||||
|
@ -22,7 +22,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"create_entry": {
|
"create_entry": {
|
||||||
"default": "Successfully connected to AlarmDecoder."
|
"default": "Successfully connected to AlarmDecoder."
|
||||||
@ -37,7 +38,7 @@
|
|||||||
"title": "Configure AlarmDecoder",
|
"title": "Configure AlarmDecoder",
|
||||||
"description": "What would you like to edit?",
|
"description": "What would you like to edit?",
|
||||||
"data": {
|
"data": {
|
||||||
"edit_select": "Edit"
|
"edit_selection": "Edit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"arm_settings": {
|
"arm_settings": {
|
||||||
|
@ -26,6 +26,7 @@ from homeassistant.components import (
|
|||||||
)
|
)
|
||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
|
AlarmControlPanelState,
|
||||||
CodeFormat,
|
CodeFormat,
|
||||||
)
|
)
|
||||||
from homeassistant.components.climate import HVACMode
|
from homeassistant.components.climate import HVACMode
|
||||||
@ -36,10 +37,6 @@ from homeassistant.const import (
|
|||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
STATE_ALARM_ARMED_AWAY,
|
|
||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
|
||||||
STATE_ALARM_ARMED_HOME,
|
|
||||||
STATE_ALARM_ARMED_NIGHT,
|
|
||||||
STATE_IDLE,
|
STATE_IDLE,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
@ -1317,13 +1314,13 @@ class AlexaSecurityPanelController(AlexaCapability):
|
|||||||
raise UnsupportedProperty(name)
|
raise UnsupportedProperty(name)
|
||||||
|
|
||||||
arm_state = self.entity.state
|
arm_state = self.entity.state
|
||||||
if arm_state == STATE_ALARM_ARMED_HOME:
|
if arm_state == AlarmControlPanelState.ARMED_HOME:
|
||||||
return "ARMED_STAY"
|
return "ARMED_STAY"
|
||||||
if arm_state == STATE_ALARM_ARMED_AWAY:
|
if arm_state == AlarmControlPanelState.ARMED_AWAY:
|
||||||
return "ARMED_AWAY"
|
return "ARMED_AWAY"
|
||||||
if arm_state == STATE_ALARM_ARMED_NIGHT:
|
if arm_state == AlarmControlPanelState.ARMED_NIGHT:
|
||||||
return "ARMED_NIGHT"
|
return "ARMED_NIGHT"
|
||||||
if arm_state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
|
if arm_state == AlarmControlPanelState.ARMED_CUSTOM_BYPASS:
|
||||||
return "ARMED_STAY"
|
return "ARMED_STAY"
|
||||||
return "DISARMED"
|
return "DISARMED"
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from typing import Any
|
|||||||
|
|
||||||
from homeassistant import core as ha
|
from homeassistant import core as ha
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
|
alarm_control_panel,
|
||||||
button,
|
button,
|
||||||
camera,
|
camera,
|
||||||
climate,
|
climate,
|
||||||
@ -51,7 +52,6 @@ from homeassistant.const import (
|
|||||||
SERVICE_VOLUME_MUTE,
|
SERVICE_VOLUME_MUTE,
|
||||||
SERVICE_VOLUME_SET,
|
SERVICE_VOLUME_SET,
|
||||||
SERVICE_VOLUME_UP,
|
SERVICE_VOLUME_UP,
|
||||||
STATE_ALARM_DISARMED,
|
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import network
|
from homeassistant.helpers import network
|
||||||
@ -1083,7 +1083,7 @@ async def async_api_arm(
|
|||||||
arm_state = directive.payload["armState"]
|
arm_state = directive.payload["armState"]
|
||||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||||
|
|
||||||
if entity.state != STATE_ALARM_DISARMED:
|
if entity.state != alarm_control_panel.AlarmControlPanelState.DISARMED:
|
||||||
msg = "You must disarm the system before you can set the requested arm state."
|
msg = "You must disarm the system before you can set the requested arm state."
|
||||||
raise AlexaSecurityPanelAuthorizationRequired(msg)
|
raise AlexaSecurityPanelAuthorizationRequired(msg)
|
||||||
|
|
||||||
@ -1133,7 +1133,7 @@ async def async_api_disarm(
|
|||||||
# Per Alexa Documentation: If you receive a Disarm directive, and the
|
# Per Alexa Documentation: If you receive a Disarm directive, and the
|
||||||
# system is already disarmed, respond with a success response,
|
# system is already disarmed, respond with a success response,
|
||||||
# not an error response.
|
# not an error response.
|
||||||
if entity.state == STATE_ALARM_DISARMED:
|
if entity.state == alarm_control_panel.AlarmControlPanelState.DISARMED:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
payload = directive.payload
|
payload = directive.payload
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"site": {
|
"site": {
|
||||||
"data": {
|
"data": {
|
||||||
"site_nmi": "Site NMI",
|
"site_id": "Site NMI",
|
||||||
"site_name": "Site Name"
|
"site_name": "Site Name"
|
||||||
},
|
},
|
||||||
"description": "Select the NMI of the site you would like to add"
|
"description": "Select the NMI of the site you would like to add"
|
||||||
|
@ -10,12 +10,15 @@ from homeassistant.core import Event, HassJob, HomeAssistant, callback
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
from .analytics import Analytics
|
from .analytics import Analytics
|
||||||
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
|
||||||
"""Set up the analytics integration."""
|
"""Set up the analytics integration."""
|
||||||
@ -52,7 +55,7 @@ async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
|
|||||||
websocket_api.async_register_command(hass, websocket_analytics)
|
websocket_api.async_register_command(hass, websocket_analytics)
|
||||||
websocket_api.async_register_command(hass, websocket_analytics_preferences)
|
websocket_api.async_register_command(hass, websocket_analytics_preferences)
|
||||||
|
|
||||||
hass.data[DOMAIN] = analytics
|
hass.data[DATA_COMPONENT] = analytics
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -65,7 +68,7 @@ def websocket_analytics(
|
|||||||
msg: dict[str, Any],
|
msg: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Return analytics preferences."""
|
"""Return analytics preferences."""
|
||||||
analytics: Analytics = hass.data[DOMAIN]
|
analytics = hass.data[DATA_COMPONENT]
|
||||||
connection.send_result(
|
connection.send_result(
|
||||||
msg["id"],
|
msg["id"],
|
||||||
{ATTR_PREFERENCES: analytics.preferences, ATTR_ONBOARDED: analytics.onboarded},
|
{ATTR_PREFERENCES: analytics.preferences, ATTR_ONBOARDED: analytics.onboarded},
|
||||||
@ -87,7 +90,7 @@ async def websocket_analytics_preferences(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Update analytics preferences."""
|
"""Update analytics preferences."""
|
||||||
preferences = msg[ATTR_PREFERENCES]
|
preferences = msg[ATTR_PREFERENCES]
|
||||||
analytics: Analytics = hass.data[DOMAIN]
|
analytics = hass.data[DATA_COMPONENT]
|
||||||
|
|
||||||
await analytics.save_preferences(preferences)
|
await analytics.save_preferences(preferences)
|
||||||
await analytics.send_analytics()
|
await analytics.send_analytics()
|
||||||
|
@ -29,6 +29,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.entity_registry as er
|
import homeassistant.helpers.entity_registry as er
|
||||||
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.helpers.system_info import async_get_system_info
|
from homeassistant.helpers.system_info import async_get_system_info
|
||||||
from homeassistant.loader import (
|
from homeassistant.loader import (
|
||||||
@ -136,7 +137,7 @@ class Analytics:
|
|||||||
@property
|
@property
|
||||||
def supervisor(self) -> bool:
|
def supervisor(self) -> bool:
|
||||||
"""Return bool if a supervisor is present."""
|
"""Return bool if a supervisor is present."""
|
||||||
return hassio.is_hassio(self.hass)
|
return is_hassio(self.hass)
|
||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Load preferences."""
|
"""Load preferences."""
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "analytics",
|
"domain": "analytics",
|
||||||
"name": "Analytics",
|
"name": "Analytics",
|
||||||
"after_dependencies": ["energy", "recorder"],
|
"after_dependencies": ["energy", "hassio", "recorder"],
|
||||||
"codeowners": ["@home-assistant/core", "@ludeeus"],
|
"codeowners": ["@home-assistant/core", "@ludeeus"],
|
||||||
"dependencies": ["api", "websocket_api"],
|
"dependencies": ["api", "websocket_api"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
||||||
|
@ -27,6 +27,7 @@ from homeassistant.helpers.selector import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_TRACKED_ADDONS,
|
||||||
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
||||||
CONF_TRACKED_INTEGRATIONS,
|
CONF_TRACKED_INTEGRATIONS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -55,8 +56,12 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
|
if all(
|
||||||
CONF_TRACKED_CUSTOM_INTEGRATIONS
|
[
|
||||||
|
not user_input.get(CONF_TRACKED_ADDONS),
|
||||||
|
not user_input.get(CONF_TRACKED_INTEGRATIONS),
|
||||||
|
not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
|
||||||
|
]
|
||||||
):
|
):
|
||||||
errors["base"] = "no_integrations_selected"
|
errors["base"] = "no_integrations_selected"
|
||||||
else:
|
else:
|
||||||
@ -64,6 +69,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
title="Home Assistant Analytics Insights",
|
title="Home Assistant Analytics Insights",
|
||||||
data={},
|
data={},
|
||||||
options={
|
options={
|
||||||
|
CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
|
||||||
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
||||||
CONF_TRACKED_INTEGRATIONS, []
|
CONF_TRACKED_INTEGRATIONS, []
|
||||||
),
|
),
|
||||||
@ -77,6 +83,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
session=async_get_clientsession(self.hass)
|
session=async_get_clientsession(self.hass)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
addons = await client.get_addons()
|
||||||
integrations = await client.get_integrations()
|
integrations = await client.get_integrations()
|
||||||
custom_integrations = await client.get_custom_integrations()
|
custom_integrations = await client.get_custom_integrations()
|
||||||
except HomeassistantAnalyticsConnectionError:
|
except HomeassistantAnalyticsConnectionError:
|
||||||
@ -99,6 +106,13 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
|
vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=list(addons),
|
||||||
|
multiple=True,
|
||||||
|
sort=True,
|
||||||
|
)
|
||||||
|
),
|
||||||
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||||
SelectSelectorConfig(
|
SelectSelectorConfig(
|
||||||
options=options,
|
options=options,
|
||||||
@ -127,14 +141,19 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
|||||||
"""Manage the options."""
|
"""Manage the options."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
|
if all(
|
||||||
CONF_TRACKED_CUSTOM_INTEGRATIONS
|
[
|
||||||
|
not user_input.get(CONF_TRACKED_ADDONS),
|
||||||
|
not user_input.get(CONF_TRACKED_INTEGRATIONS),
|
||||||
|
not user_input.get(CONF_TRACKED_CUSTOM_INTEGRATIONS),
|
||||||
|
]
|
||||||
):
|
):
|
||||||
errors["base"] = "no_integrations_selected"
|
errors["base"] = "no_integrations_selected"
|
||||||
else:
|
else:
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title="",
|
title="",
|
||||||
data={
|
data={
|
||||||
|
CONF_TRACKED_ADDONS: user_input.get(CONF_TRACKED_ADDONS, []),
|
||||||
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
CONF_TRACKED_INTEGRATIONS: user_input.get(
|
||||||
CONF_TRACKED_INTEGRATIONS, []
|
CONF_TRACKED_INTEGRATIONS, []
|
||||||
),
|
),
|
||||||
@ -148,6 +167,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
|||||||
session=async_get_clientsession(self.hass)
|
session=async_get_clientsession(self.hass)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
addons = await client.get_addons()
|
||||||
integrations = await client.get_integrations()
|
integrations = await client.get_integrations()
|
||||||
custom_integrations = await client.get_custom_integrations()
|
custom_integrations = await client.get_custom_integrations()
|
||||||
except HomeassistantAnalyticsConnectionError:
|
except HomeassistantAnalyticsConnectionError:
|
||||||
@ -168,6 +188,13 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
|||||||
data_schema=self.add_suggested_values_to_schema(
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
vol.Schema(
|
vol.Schema(
|
||||||
{
|
{
|
||||||
|
vol.Optional(CONF_TRACKED_ADDONS): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=list(addons),
|
||||||
|
multiple=True,
|
||||||
|
sort=True,
|
||||||
|
)
|
||||||
|
),
|
||||||
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
|
||||||
SelectSelectorConfig(
|
SelectSelectorConfig(
|
||||||
options=options,
|
options=options,
|
||||||
|
@ -4,6 +4,7 @@ import logging
|
|||||||
|
|
||||||
DOMAIN = "analytics_insights"
|
DOMAIN = "analytics_insights"
|
||||||
|
|
||||||
|
CONF_TRACKED_ADDONS = "tracked_addons"
|
||||||
CONF_TRACKED_INTEGRATIONS = "tracked_integrations"
|
CONF_TRACKED_INTEGRATIONS = "tracked_integrations"
|
||||||
CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations"
|
CONF_TRACKED_CUSTOM_INTEGRATIONS = "tracked_custom_integrations"
|
||||||
|
|
||||||
|
@ -12,11 +12,13 @@ from python_homeassistant_analytics import (
|
|||||||
HomeassistantAnalyticsConnectionError,
|
HomeassistantAnalyticsConnectionError,
|
||||||
HomeassistantAnalyticsNotModifiedError,
|
HomeassistantAnalyticsNotModifiedError,
|
||||||
)
|
)
|
||||||
|
from python_homeassistant_analytics.models import Addon
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_TRACKED_ADDONS,
|
||||||
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
||||||
CONF_TRACKED_INTEGRATIONS,
|
CONF_TRACKED_INTEGRATIONS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -31,6 +33,9 @@ if TYPE_CHECKING:
|
|||||||
class AnalyticsData:
|
class AnalyticsData:
|
||||||
"""Analytics data class."""
|
"""Analytics data class."""
|
||||||
|
|
||||||
|
active_installations: int
|
||||||
|
reports_integrations: int
|
||||||
|
addons: dict[str, int]
|
||||||
core_integrations: dict[str, int]
|
core_integrations: dict[str, int]
|
||||||
custom_integrations: dict[str, int]
|
custom_integrations: dict[str, int]
|
||||||
|
|
||||||
@ -51,6 +56,7 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
|||||||
update_interval=timedelta(hours=12),
|
update_interval=timedelta(hours=12),
|
||||||
)
|
)
|
||||||
self._client = client
|
self._client = client
|
||||||
|
self._tracked_addons = self.config_entry.options.get(CONF_TRACKED_ADDONS, [])
|
||||||
self._tracked_integrations = self.config_entry.options[
|
self._tracked_integrations = self.config_entry.options[
|
||||||
CONF_TRACKED_INTEGRATIONS
|
CONF_TRACKED_INTEGRATIONS
|
||||||
]
|
]
|
||||||
@ -60,6 +66,7 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
|||||||
|
|
||||||
async def _async_update_data(self) -> AnalyticsData:
|
async def _async_update_data(self) -> AnalyticsData:
|
||||||
try:
|
try:
|
||||||
|
addons_data = await self._client.get_addons()
|
||||||
data = await self._client.get_current_analytics()
|
data = await self._client.get_current_analytics()
|
||||||
custom_data = await self._client.get_custom_integrations()
|
custom_data = await self._client.get_custom_integrations()
|
||||||
except HomeassistantAnalyticsConnectionError as err:
|
except HomeassistantAnalyticsConnectionError as err:
|
||||||
@ -68,6 +75,9 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
|||||||
) from err
|
) from err
|
||||||
except HomeassistantAnalyticsNotModifiedError:
|
except HomeassistantAnalyticsNotModifiedError:
|
||||||
return self.data
|
return self.data
|
||||||
|
addons = {
|
||||||
|
addon: get_addon_value(addons_data, addon) for addon in self._tracked_addons
|
||||||
|
}
|
||||||
core_integrations = {
|
core_integrations = {
|
||||||
integration: data.integrations.get(integration, 0)
|
integration: data.integrations.get(integration, 0)
|
||||||
for integration in self._tracked_integrations
|
for integration in self._tracked_integrations
|
||||||
@ -76,7 +86,20 @@ class HomeassistantAnalyticsDataUpdateCoordinator(DataUpdateCoordinator[Analytic
|
|||||||
integration: get_custom_integration_value(custom_data, integration)
|
integration: get_custom_integration_value(custom_data, integration)
|
||||||
for integration in self._tracked_custom_integrations
|
for integration in self._tracked_custom_integrations
|
||||||
}
|
}
|
||||||
return AnalyticsData(core_integrations, custom_integrations)
|
return AnalyticsData(
|
||||||
|
data.active_installations,
|
||||||
|
data.reports_integrations,
|
||||||
|
addons,
|
||||||
|
core_integrations,
|
||||||
|
custom_integrations,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_addon_value(data: dict[str, Addon], name_slug: str) -> int:
|
||||||
|
"""Get addon value."""
|
||||||
|
if name_slug in data:
|
||||||
|
return data[name_slug].total
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def get_custom_integration_value(
|
def get_custom_integration_value(
|
||||||
|
@ -6,6 +6,12 @@
|
|||||||
},
|
},
|
||||||
"custom_integrations": {
|
"custom_integrations": {
|
||||||
"default": "mdi:puzzle-edit"
|
"default": "mdi:puzzle-edit"
|
||||||
|
},
|
||||||
|
"total_active_installations": {
|
||||||
|
"default": "mdi:puzzle"
|
||||||
|
},
|
||||||
|
"total_reports_integrations": {
|
||||||
|
"default": "mdi:puzzle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,6 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["python_homeassistant_analytics"],
|
"loggers": ["python_homeassistant_analytics"],
|
||||||
"requirements": ["python-homeassistant-analytics==0.7.0"],
|
"requirements": ["python-homeassistant-analytics==0.8.0"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,20 @@ class AnalyticsSensorEntityDescription(SensorEntityDescription):
|
|||||||
value_fn: Callable[[AnalyticsData], StateType]
|
value_fn: Callable[[AnalyticsData], StateType]
|
||||||
|
|
||||||
|
|
||||||
|
def get_addon_entity_description(
|
||||||
|
name_slug: str,
|
||||||
|
) -> AnalyticsSensorEntityDescription:
|
||||||
|
"""Get addon entity description."""
|
||||||
|
return AnalyticsSensorEntityDescription(
|
||||||
|
key=f"addon_{name_slug}_active_installations",
|
||||||
|
translation_key="addons",
|
||||||
|
name=name_slug,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
native_unit_of_measurement="active installations",
|
||||||
|
value_fn=lambda data: data.addons.get(name_slug),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_core_integration_entity_description(
|
def get_core_integration_entity_description(
|
||||||
domain: str, name: str
|
domain: str, name: str
|
||||||
) -> AnalyticsSensorEntityDescription:
|
) -> AnalyticsSensorEntityDescription:
|
||||||
@ -57,6 +71,26 @@ def get_custom_integration_entity_description(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
GENERAL_SENSORS = [
|
||||||
|
AnalyticsSensorEntityDescription(
|
||||||
|
key="total_active_installations",
|
||||||
|
translation_key="total_active_installations",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
native_unit_of_measurement="active installations",
|
||||||
|
value_fn=lambda data: data.active_installations,
|
||||||
|
),
|
||||||
|
AnalyticsSensorEntityDescription(
|
||||||
|
key="total_reports_integrations",
|
||||||
|
translation_key="total_reports_integrations",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
native_unit_of_measurement="active installations",
|
||||||
|
value_fn=lambda data: data.reports_integrations,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: AnalyticsInsightsConfigEntry,
|
entry: AnalyticsInsightsConfigEntry,
|
||||||
@ -69,6 +103,13 @@ async def async_setup_entry(
|
|||||||
analytics_data.coordinator
|
analytics_data.coordinator
|
||||||
)
|
)
|
||||||
entities: list[HomeassistantAnalyticsSensor] = []
|
entities: list[HomeassistantAnalyticsSensor] = []
|
||||||
|
entities.extend(
|
||||||
|
HomeassistantAnalyticsSensor(
|
||||||
|
coordinator,
|
||||||
|
get_addon_entity_description(addon_name_slug),
|
||||||
|
)
|
||||||
|
for addon_name_slug in coordinator.data.addons
|
||||||
|
)
|
||||||
entities.extend(
|
entities.extend(
|
||||||
HomeassistantAnalyticsSensor(
|
HomeassistantAnalyticsSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
@ -85,6 +126,12 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
for integration_domain in coordinator.data.custom_integrations
|
for integration_domain in coordinator.data.custom_integrations
|
||||||
)
|
)
|
||||||
|
|
||||||
|
entities.extend(
|
||||||
|
HomeassistantAnalyticsSensor(coordinator, entity_description)
|
||||||
|
for entity_description in GENERAL_SENSORS
|
||||||
|
)
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,10 +3,12 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"tracked_addons": "Addons",
|
||||||
"tracked_integrations": "Integrations",
|
"tracked_integrations": "Integrations",
|
||||||
"tracked_custom_integrations": "Custom integrations"
|
"tracked_custom_integrations": "Custom integrations"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
|
"tracked_addons": "Select the addons you want to track",
|
||||||
"tracked_integrations": "Select the integrations you want to track",
|
"tracked_integrations": "Select the integrations you want to track",
|
||||||
"tracked_custom_integrations": "Select the custom integrations you want to track"
|
"tracked_custom_integrations": "Select the custom integrations you want to track"
|
||||||
}
|
}
|
||||||
@ -17,17 +19,19 @@
|
|||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"no_integration_selected": "You must select at least one integration to track"
|
"no_integrations_selected": "You must select at least one integration to track"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"tracked_addons": "[%key:component::analytics_insights::config::step::user::data::tracked_addons%]",
|
||||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]",
|
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]",
|
||||||
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]"
|
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
|
"tracked_addons": "[%key:component::analytics_insights::config::step::user::data_description::tracked_addons%]",
|
||||||
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]",
|
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]",
|
||||||
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]"
|
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]"
|
||||||
}
|
}
|
||||||
@ -37,13 +41,19 @@
|
|||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"no_integration_selected": "[%key:component::analytics_insights::config::error::no_integration_selected%]"
|
"no_integrations_selected": "[%key:component::analytics_insights::config::error::no_integrations_selected%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"custom_integrations": {
|
"custom_integrations": {
|
||||||
"name": "{custom_integration_domain} (custom)"
|
"name": "{custom_integration_domain} (custom)"
|
||||||
|
},
|
||||||
|
"total_active_installations": {
|
||||||
|
"name": "Total active installations"
|
||||||
|
},
|
||||||
|
"total_reports_integrations": {
|
||||||
|
"name": "Total reported integrations"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pydroid_ipcam import PyDroidIPCam
|
from pydroid_ipcam import PyDroidIPCam
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
@ -15,8 +14,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
|
||||||
from .coordinator import AndroidIPCamDataUpdateCoordinator
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [
|
PLATFORMS: list[Platform] = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
@ -26,7 +24,9 @@ PLATFORMS: list[Platform] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: AndroidIPCamConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Set up Android IP Webcam from a config entry."""
|
"""Set up Android IP Webcam from a config entry."""
|
||||||
websession = async_get_clientsession(hass)
|
websession = async_get_clientsession(hass)
|
||||||
cam = PyDroidIPCam(
|
cam = PyDroidIPCam(
|
||||||
@ -40,16 +40,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
coordinator = AndroidIPCamDataUpdateCoordinator(hass, entry, cam)
|
coordinator = AndroidIPCamDataUpdateCoordinator(hass, entry, cam)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: AndroidIPCamConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
@ -7,12 +7,11 @@ from homeassistant.components.binary_sensor import (
|
|||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN, MOTION_ACTIVE
|
from .const import MOTION_ACTIVE
|
||||||
from .coordinator import AndroidIPCamDataUpdateCoordinator
|
from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
|
||||||
from .entity import AndroidIPCamBaseEntity
|
from .entity import AndroidIPCamBaseEntity
|
||||||
|
|
||||||
BINARY_SENSOR_DESCRIPTION = BinarySensorEntityDescription(
|
BINARY_SENSOR_DESCRIPTION = BinarySensorEntityDescription(
|
||||||
@ -24,16 +23,12 @@ BINARY_SENSOR_DESCRIPTION = BinarySensorEntityDescription(
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AndroidIPCamConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the IP Webcam sensors from config entry."""
|
"""Set up the IP Webcam sensors from config entry."""
|
||||||
|
|
||||||
coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][
|
async_add_entities([IPWebcamBinarySensor(config_entry.runtime_data)])
|
||||||
config_entry.entry_id
|
|
||||||
]
|
|
||||||
|
|
||||||
async_add_entities([IPWebcamBinarySensor(coordinator)])
|
|
||||||
|
|
||||||
|
|
||||||
class IPWebcamBinarySensor(AndroidIPCamBaseEntity, BinarySensorEntity):
|
class IPWebcamBinarySensor(AndroidIPCamBaseEntity, BinarySensorEntity):
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
@ -15,21 +14,17 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AndroidIPCamDataUpdateCoordinator
|
from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AndroidIPCamConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the IP Webcam camera from config entry."""
|
"""Set up the IP Webcam camera from config entry."""
|
||||||
filter_urllib3_logging()
|
filter_urllib3_logging()
|
||||||
coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][
|
async_add_entities([IPWebcamCamera(config_entry.runtime_data)])
|
||||||
config_entry.entry_id
|
|
||||||
]
|
|
||||||
|
|
||||||
async_add_entities([IPWebcamCamera(coordinator)])
|
|
||||||
|
|
||||||
|
|
||||||
class IPWebcamCamera(MjpegCamera):
|
class IPWebcamCamera(MjpegCamera):
|
||||||
|
@ -15,19 +15,22 @@ from .const import DOMAIN
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type AndroidIPCamConfigEntry = ConfigEntry[AndroidIPCamDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||||
"""Coordinator class for the Android IP Webcam."""
|
"""Coordinator class for the Android IP Webcam."""
|
||||||
|
|
||||||
|
config_entry: AndroidIPCamConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AndroidIPCamConfigEntry,
|
||||||
cam: PyDroidIPCam,
|
cam: PyDroidIPCam,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Android IP Webcam."""
|
"""Initialize the Android IP Webcam."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.config_entry: ConfigEntry = config_entry
|
|
||||||
self.cam = cam
|
self.cam = cam
|
||||||
super().__init__(
|
super().__init__(
|
||||||
self.hass,
|
self.hass,
|
||||||
|
@ -13,14 +13,12 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
|
||||||
from .coordinator import AndroidIPCamDataUpdateCoordinator
|
|
||||||
from .entity import AndroidIPCamBaseEntity
|
from .entity import AndroidIPCamBaseEntity
|
||||||
|
|
||||||
|
|
||||||
@ -120,19 +118,21 @@ SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AndroidIPCamConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the IP Webcam sensors from config entry."""
|
"""Set up the IP Webcam sensors from config entry."""
|
||||||
|
|
||||||
coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][
|
coordinator = config_entry.runtime_data
|
||||||
config_entry.entry_id
|
|
||||||
]
|
|
||||||
sensor_types = [
|
sensor_types = [
|
||||||
sensor
|
sensor
|
||||||
for sensor in SENSOR_TYPES
|
for sensor in SENSOR_TYPES
|
||||||
if sensor.key
|
if sensor.key
|
||||||
in [*coordinator.cam.enabled_sensors, "audio_connections", "video_connections"]
|
in [
|
||||||
|
*coordinator.cam.enabled_sensors,
|
||||||
|
"audio_connections",
|
||||||
|
"video_connections",
|
||||||
|
]
|
||||||
]
|
]
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
IPWebcamSensor(coordinator, description) for description in sensor_types
|
IPWebcamSensor(coordinator, description) for description in sensor_types
|
||||||
|
@ -9,13 +9,11 @@ from typing import Any
|
|||||||
from pydroid_ipcam import PyDroidIPCam
|
from pydroid_ipcam import PyDroidIPCam
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .coordinator import AndroidIPCamConfigEntry, AndroidIPCamDataUpdateCoordinator
|
||||||
from .coordinator import AndroidIPCamDataUpdateCoordinator
|
|
||||||
from .entity import AndroidIPCamBaseEntity
|
from .entity import AndroidIPCamBaseEntity
|
||||||
|
|
||||||
|
|
||||||
@ -113,14 +111,12 @@ SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AndroidIPCamConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the IP Webcam switches from config entry."""
|
"""Set up the IP Webcam switches from config entry."""
|
||||||
|
|
||||||
coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][
|
coordinator = config_entry.runtime_data
|
||||||
config_entry.entry_id
|
|
||||||
]
|
|
||||||
switch_types = [
|
switch_types = [
|
||||||
switch
|
switch
|
||||||
for switch in SWITCH_TYPES
|
for switch in SWITCH_TYPES
|
||||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ from .const import (
|
|||||||
CONF_ADB_SERVER_IP,
|
CONF_ADB_SERVER_IP,
|
||||||
CONF_ADB_SERVER_PORT,
|
CONF_ADB_SERVER_PORT,
|
||||||
CONF_ADBKEY,
|
CONF_ADBKEY,
|
||||||
|
CONF_SCREENCAP_INTERVAL,
|
||||||
CONF_STATE_DETECTION_RULES,
|
CONF_STATE_DETECTION_RULES,
|
||||||
DEFAULT_ADB_SERVER_PORT,
|
DEFAULT_ADB_SERVER_PORT,
|
||||||
DEVICE_ANDROIDTV,
|
DEVICE_ANDROIDTV,
|
||||||
@ -66,6 +68,8 @@ RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
|
|||||||
|
|
||||||
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
|
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AndroidTVRuntimeData:
|
class AndroidTVRuntimeData:
|
||||||
@ -157,6 +161,32 @@ async def async_connect_androidtv(
|
|||||||
return aftv, None
|
return aftv, None
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Migrate old entry."""
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Migrating configuration from version %s.%s", entry.version, entry.minor_version
|
||||||
|
)
|
||||||
|
|
||||||
|
if entry.version == 1:
|
||||||
|
new_options = {**entry.options}
|
||||||
|
|
||||||
|
# Migrate MinorVersion 1 -> MinorVersion 2: New option
|
||||||
|
if entry.minor_version < 2:
|
||||||
|
new_options = {**new_options, CONF_SCREENCAP_INTERVAL: 0}
|
||||||
|
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry, options=new_options, minor_version=2, version=1
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Migration to configuration version %s.%s successful",
|
||||||
|
entry.version,
|
||||||
|
entry.minor_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool:
|
||||||
"""Set up Android Debug Bridge platform."""
|
"""Set up Android Debug Bridge platform."""
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ from .const import (
|
|||||||
CONF_APPS,
|
CONF_APPS,
|
||||||
CONF_EXCLUDE_UNNAMED_APPS,
|
CONF_EXCLUDE_UNNAMED_APPS,
|
||||||
CONF_GET_SOURCES,
|
CONF_GET_SOURCES,
|
||||||
CONF_SCREENCAP,
|
CONF_SCREENCAP_INTERVAL,
|
||||||
CONF_STATE_DETECTION_RULES,
|
CONF_STATE_DETECTION_RULES,
|
||||||
CONF_TURN_OFF_COMMAND,
|
CONF_TURN_OFF_COMMAND,
|
||||||
CONF_TURN_ON_COMMAND,
|
CONF_TURN_ON_COMMAND,
|
||||||
@ -43,7 +43,7 @@ from .const import (
|
|||||||
DEFAULT_EXCLUDE_UNNAMED_APPS,
|
DEFAULT_EXCLUDE_UNNAMED_APPS,
|
||||||
DEFAULT_GET_SOURCES,
|
DEFAULT_GET_SOURCES,
|
||||||
DEFAULT_PORT,
|
DEFAULT_PORT,
|
||||||
DEFAULT_SCREENCAP,
|
DEFAULT_SCREENCAP_INTERVAL,
|
||||||
DEVICE_CLASSES,
|
DEVICE_CLASSES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PROP_ETHMAC,
|
PROP_ETHMAC,
|
||||||
@ -76,6 +76,7 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow."""
|
"""Handle a config flow."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
MINOR_VERSION = 2
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _show_setup_form(
|
def _show_setup_form(
|
||||||
@ -253,10 +254,12 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry):
|
|||||||
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
|
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
|
||||||
),
|
),
|
||||||
): bool,
|
): bool,
|
||||||
vol.Optional(
|
vol.Required(
|
||||||
CONF_SCREENCAP,
|
CONF_SCREENCAP_INTERVAL,
|
||||||
default=options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP),
|
default=options.get(
|
||||||
): bool,
|
CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
|
||||||
|
),
|
||||||
|
): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=15)),
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_TURN_OFF_COMMAND,
|
CONF_TURN_OFF_COMMAND,
|
||||||
description={
|
description={
|
||||||
|
@ -9,6 +9,7 @@ CONF_APPS = "apps"
|
|||||||
CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps"
|
CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps"
|
||||||
CONF_GET_SOURCES = "get_sources"
|
CONF_GET_SOURCES = "get_sources"
|
||||||
CONF_SCREENCAP = "screencap"
|
CONF_SCREENCAP = "screencap"
|
||||||
|
CONF_SCREENCAP_INTERVAL = "screencap_interval"
|
||||||
CONF_STATE_DETECTION_RULES = "state_detection_rules"
|
CONF_STATE_DETECTION_RULES = "state_detection_rules"
|
||||||
CONF_TURN_OFF_COMMAND = "turn_off_command"
|
CONF_TURN_OFF_COMMAND = "turn_off_command"
|
||||||
CONF_TURN_ON_COMMAND = "turn_on_command"
|
CONF_TURN_ON_COMMAND = "turn_on_command"
|
||||||
@ -18,7 +19,7 @@ DEFAULT_DEVICE_CLASS = "auto"
|
|||||||
DEFAULT_EXCLUDE_UNNAMED_APPS = False
|
DEFAULT_EXCLUDE_UNNAMED_APPS = False
|
||||||
DEFAULT_GET_SOURCES = True
|
DEFAULT_GET_SOURCES = True
|
||||||
DEFAULT_PORT = 5555
|
DEFAULT_PORT = 5555
|
||||||
DEFAULT_SCREENCAP = True
|
DEFAULT_SCREENCAP_INTERVAL = 5
|
||||||
|
|
||||||
DEVICE_ANDROIDTV = "androidtv"
|
DEVICE_ANDROIDTV = "androidtv"
|
||||||
DEVICE_FIRETV = "firetv"
|
DEVICE_FIRETV = "firetv"
|
||||||
|
@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from androidtv.constants import APPS, KEYS
|
from androidtv.constants import APPS, KEYS
|
||||||
from androidtv.setup_async import AndroidTVAsync, FireTVAsync
|
from androidtv.setup_async import AndroidTVAsync, FireTVAsync
|
||||||
@ -23,19 +22,19 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from . import AndroidTVConfigEntry
|
from . import AndroidTVConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_APPS,
|
CONF_APPS,
|
||||||
CONF_EXCLUDE_UNNAMED_APPS,
|
CONF_EXCLUDE_UNNAMED_APPS,
|
||||||
CONF_GET_SOURCES,
|
CONF_GET_SOURCES,
|
||||||
CONF_SCREENCAP,
|
CONF_SCREENCAP_INTERVAL,
|
||||||
CONF_TURN_OFF_COMMAND,
|
CONF_TURN_OFF_COMMAND,
|
||||||
CONF_TURN_ON_COMMAND,
|
CONF_TURN_ON_COMMAND,
|
||||||
DEFAULT_EXCLUDE_UNNAMED_APPS,
|
DEFAULT_EXCLUDE_UNNAMED_APPS,
|
||||||
DEFAULT_GET_SOURCES,
|
DEFAULT_GET_SOURCES,
|
||||||
DEFAULT_SCREENCAP,
|
DEFAULT_SCREENCAP_INTERVAL,
|
||||||
DEVICE_ANDROIDTV,
|
DEVICE_ANDROIDTV,
|
||||||
SIGNAL_CONFIG_ENTITY,
|
SIGNAL_CONFIG_ENTITY,
|
||||||
)
|
)
|
||||||
@ -48,8 +47,6 @@ ATTR_DEVICE_PATH = "device_path"
|
|||||||
ATTR_HDMI_INPUT = "hdmi_input"
|
ATTR_HDMI_INPUT = "hdmi_input"
|
||||||
ATTR_LOCAL_PATH = "local_path"
|
ATTR_LOCAL_PATH = "local_path"
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCREENCAPS = timedelta(seconds=60)
|
|
||||||
|
|
||||||
SERVICE_ADB_COMMAND = "adb_command"
|
SERVICE_ADB_COMMAND = "adb_command"
|
||||||
SERVICE_DOWNLOAD = "download"
|
SERVICE_DOWNLOAD = "download"
|
||||||
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
|
SERVICE_LEARN_SENDEVENT = "learn_sendevent"
|
||||||
@ -125,7 +122,8 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
|||||||
self._app_name_to_id: dict[str, str] = {}
|
self._app_name_to_id: dict[str, str] = {}
|
||||||
self._get_sources = DEFAULT_GET_SOURCES
|
self._get_sources = DEFAULT_GET_SOURCES
|
||||||
self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS
|
self._exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS
|
||||||
self._screencap = DEFAULT_SCREENCAP
|
self._screencap_delta: timedelta | None = None
|
||||||
|
self._last_screencap: datetime | None = None
|
||||||
self.turn_on_command: str | None = None
|
self.turn_on_command: str | None = None
|
||||||
self.turn_off_command: str | None = None
|
self.turn_off_command: str | None = None
|
||||||
|
|
||||||
@ -159,7 +157,13 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
|||||||
self._exclude_unnamed_apps = options.get(
|
self._exclude_unnamed_apps = options.get(
|
||||||
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
|
CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
|
||||||
)
|
)
|
||||||
self._screencap = options.get(CONF_SCREENCAP, DEFAULT_SCREENCAP)
|
screencap_interval: int = options.get(
|
||||||
|
CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
|
||||||
|
)
|
||||||
|
if screencap_interval > 0:
|
||||||
|
self._screencap_delta = timedelta(minutes=screencap_interval)
|
||||||
|
else:
|
||||||
|
self._screencap_delta = None
|
||||||
self.turn_off_command = options.get(CONF_TURN_OFF_COMMAND)
|
self.turn_off_command = options.get(CONF_TURN_OFF_COMMAND)
|
||||||
self.turn_on_command = options.get(CONF_TURN_ON_COMMAND)
|
self.turn_on_command = options.get(CONF_TURN_ON_COMMAND)
|
||||||
|
|
||||||
@ -183,7 +187,7 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
|||||||
async def _async_get_screencap(self, prev_app_id: str | None = None) -> None:
|
async def _async_get_screencap(self, prev_app_id: str | None = None) -> None:
|
||||||
"""Take a screen capture from the device when enabled."""
|
"""Take a screen capture from the device when enabled."""
|
||||||
if (
|
if (
|
||||||
not self._screencap
|
not self._screencap_delta
|
||||||
or self.state in {MediaPlayerState.OFF, None}
|
or self.state in {MediaPlayerState.OFF, None}
|
||||||
or not self.available
|
or not self.available
|
||||||
):
|
):
|
||||||
@ -193,11 +197,18 @@ class ADBDevice(AndroidTVEntity, MediaPlayerEntity):
|
|||||||
force: bool = prev_app_id is not None
|
force: bool = prev_app_id is not None
|
||||||
if force:
|
if force:
|
||||||
force = prev_app_id != self._attr_app_id
|
force = prev_app_id != self._attr_app_id
|
||||||
await self._adb_get_screencap(no_throttle=force)
|
await self._adb_get_screencap(force)
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCREENCAPS)
|
async def _adb_get_screencap(self, force: bool = False) -> None:
|
||||||
async def _adb_get_screencap(self, **kwargs: Any) -> None:
|
"""Take a screen capture from the device every configured minutes."""
|
||||||
"""Take a screen capture from the device every 60 seconds."""
|
time_elapsed = self._screencap_delta is not None and (
|
||||||
|
self._last_screencap is None
|
||||||
|
or (utcnow() - self._last_screencap) >= self._screencap_delta
|
||||||
|
)
|
||||||
|
if not (force or time_elapsed):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._last_screencap = utcnow()
|
||||||
if media_data := await self._adb_screencap():
|
if media_data := await self._adb_screencap():
|
||||||
self._media_image = media_data, "image/png"
|
self._media_image = media_data, "image/png"
|
||||||
self._attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16]
|
self._attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16]
|
||||||
|
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