mirror of
https://github.com/home-assistant/core.git
synced 2025-12-18 22:08:14 +00:00
Compare commits
88 Commits
input_bool
...
rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0547153730 | ||
|
|
eb024b4dde | ||
|
|
1d4817608e | ||
|
|
a37ca293e1 | ||
|
|
f3dbddee16 | ||
|
|
b26681ee88 | ||
|
|
effe72bfda | ||
|
|
076835ca1c | ||
|
|
4b9b1e611a | ||
|
|
0b4ea42810 | ||
|
|
8907608345 | ||
|
|
356ee07e22 | ||
|
|
bee3ee6320 | ||
|
|
fb72ff9bd0 | ||
|
|
412e05d8da | ||
|
|
58ee8e863e | ||
|
|
e3a47bfc51 | ||
|
|
a6cdacc8fe | ||
|
|
dd0425ab8e | ||
|
|
1d289c0083 | ||
|
|
70786a1d90 | ||
|
|
293eb69788 | ||
|
|
71d92291d1 | ||
|
|
726de64394 | ||
|
|
de04f22f89 | ||
|
|
9e8cc3a65b | ||
|
|
27fa92b607 | ||
|
|
ce5c5c5eb7 | ||
|
|
a2b5744696 | ||
|
|
201c3785f5 | ||
|
|
24de26cbf5 | ||
|
|
ac0a544829 | ||
|
|
1a11b92f05 | ||
|
|
ab0811f59f | ||
|
|
68711b2f21 | ||
|
|
886e2b0af1 | ||
|
|
7492b5be75 | ||
|
|
e4f1565e3c | ||
|
|
7f37412199 | ||
|
|
eaef0160a2 | ||
|
|
f049c425ba | ||
|
|
50eee75b8f | ||
|
|
81e47f6844 | ||
|
|
ffebbab020 | ||
|
|
9824bdc1c9 | ||
|
|
a933d4a0eb | ||
|
|
f7f7f9a2de | ||
|
|
aac412f3a8 | ||
|
|
660a14e78d | ||
|
|
0b52c806d4 | ||
|
|
bbe27d86a1 | ||
|
|
fb7941df1d | ||
|
|
c46e341941 | ||
|
|
2e3a9e3a90 | ||
|
|
55c5ecd28a | ||
|
|
e50e2487e1 | ||
|
|
74e118f85c | ||
|
|
39a62ec2f6 | ||
|
|
1310efcb07 | ||
|
|
53af592c2c | ||
|
|
023987b805 | ||
|
|
5b8fb607b4 | ||
|
|
252f6716ff | ||
|
|
bf78e28f83 | ||
|
|
22706d02a7 | ||
|
|
5cff0e946a | ||
|
|
6cbe2ed279 | ||
|
|
fb0f5f52b2 | ||
|
|
5c422bb770 | ||
|
|
fd1bc07b8c | ||
|
|
97a019d313 | ||
|
|
8ae8a564c2 | ||
|
|
2f72f57bb7 | ||
|
|
e928e3cb54 | ||
|
|
b0e2109e15 | ||
|
|
b449c6673f | ||
|
|
877ad38ac3 | ||
|
|
229f45feae | ||
|
|
a535d1f4eb | ||
|
|
d4adc00ae6 | ||
|
|
ba141f9d1d | ||
|
|
72be9793a4 | ||
|
|
5ae7cc5f84 | ||
|
|
d01a469b46 | ||
|
|
9f07052874 | ||
|
|
b9bc9d3fc2 | ||
|
|
1e180cd5ee | ||
|
|
dc9cdd13b1 |
@@ -13,7 +13,6 @@ core: &core
|
|||||||
|
|
||||||
# Our base platforms, that are used by other integrations
|
# Our base platforms, that are used by other integrations
|
||||||
base_platforms: &base_platforms
|
base_platforms: &base_platforms
|
||||||
- homeassistant/components/ai_task/**
|
|
||||||
- homeassistant/components/air_quality/**
|
- homeassistant/components/air_quality/**
|
||||||
- homeassistant/components/alarm_control_panel/**
|
- homeassistant/components/alarm_control_panel/**
|
||||||
- homeassistant/components/assist_satellite/**
|
- homeassistant/components/assist_satellite/**
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"charliermarsh.ruff",
|
"charliermarsh.ruff",
|
||||||
"ms-python.pylint",
|
"ms-python.pylint",
|
||||||
"ms-python.vscode-pylance",
|
"ms-python.vscode-pylance",
|
||||||
|
"visualstudioexptteam.vscodeintellicode",
|
||||||
"redhat.vscode-yaml",
|
"redhat.vscode-yaml",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"GitHub.vscode-pull-request-github",
|
"GitHub.vscode-pull-request-github",
|
||||||
|
|||||||
34
.github/workflows/builder.yml
vendored
34
.github/workflows/builder.yml
vendored
@@ -15,7 +15,7 @@ env:
|
|||||||
UV_HTTP_TIMEOUT: 60
|
UV_HTTP_TIMEOUT: 60
|
||||||
UV_SYSTEM_PYTHON: "true"
|
UV_SYSTEM_PYTHON: "true"
|
||||||
# Base image version from https://github.com/home-assistant/docker
|
# Base image version from https://github.com/home-assistant/docker
|
||||||
BASE_IMAGE_VERSION: "2025.12.0"
|
BASE_IMAGE_VERSION: "2025.11.3"
|
||||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
architectures: ${{ env.ARCHITECTURES }}
|
architectures: ${{ env.ARCHITECTURES }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
@@ -70,7 +70,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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
@@ -96,7 +96,7 @@ jobs:
|
|||||||
os: ubuntu-24.04-arm
|
os: ubuntu-24.04-arm
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
@@ -169,7 +169,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -190,8 +190,7 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- &install_cosign
|
- name: Install Cosign
|
||||||
name: Install Cosign
|
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.5.3"
|
cosign-release: "v2.5.3"
|
||||||
@@ -273,7 +272,7 @@ jobs:
|
|||||||
- green
|
- green
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
@@ -295,7 +294,7 @@ jobs:
|
|||||||
|
|
||||||
# home-assistant/builder doesn't support sha pinning
|
# home-assistant/builder doesn't support sha pinning
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
uses: home-assistant/builder@2025.11.0
|
uses: home-assistant/builder@2025.09.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
@@ -311,7 +310,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
uses: home-assistant/actions/helpers/git-init@master
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
@@ -354,7 +353,10 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
steps:
|
steps:
|
||||||
- *install_cosign
|
- name: Install Cosign
|
||||||
|
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||||
|
with:
|
||||||
|
cosign-release: "v2.2.3"
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
@@ -391,7 +393,7 @@ jobs:
|
|||||||
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
||||||
- name: Generate Docker metadata
|
- name: Generate Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
|
||||||
with:
|
with:
|
||||||
images: ${{ matrix.registry }}/home-assistant
|
images: ${{ matrix.registry }}/home-assistant
|
||||||
sep-tags: ","
|
sep-tags: ","
|
||||||
@@ -405,7 +407,7 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.7.1
|
uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 # v3.7.1
|
||||||
|
|
||||||
- name: Copy architecture images to DockerHub
|
- name: Copy architecture images to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
@@ -474,7 +476,7 @@ 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
@@ -482,7 +484,7 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -519,7 +521,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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
|
|||||||
22
.github/workflows/ci.yaml
vendored
22
.github/workflows/ci.yaml
vendored
@@ -40,9 +40,9 @@ env:
|
|||||||
CACHE_VERSION: 2
|
CACHE_VERSION: 2
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2026.1"
|
HA_SHORT_VERSION: "2025.12"
|
||||||
DEFAULT_PYTHON: "3.13.11"
|
DEFAULT_PYTHON: "3.13.9"
|
||||||
ALL_PYTHON_VERSIONS: "['3.13.11', '3.14.2']"
|
ALL_PYTHON_VERSIONS: "['3.13.9', '3.14.0']"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||||
# 10.6 is the current long-term-support
|
# 10.6 is the current long-term-support
|
||||||
@@ -99,7 +99,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- &checkout
|
- &checkout
|
||||||
name: Check out code from GitHub
|
name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
- 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: |
|
||||||
@@ -263,7 +263,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: &actions-cache actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
uses: &actions-cache actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: &key-pre-commit-venv >-
|
key: &key-pre-commit-venv >-
|
||||||
@@ -304,7 +304,7 @@ jobs:
|
|||||||
- &cache-restore-pre-commit-venv
|
- &cache-restore-pre-commit-venv
|
||||||
name: Restore base Python virtual environment
|
name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: &actions-cache-restore actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
uses: &actions-cache-restore actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -511,7 +511,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Save apt cache
|
- name: Save apt cache
|
||||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||||
uses: &actions-cache-save actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
uses: &actions-cache-save actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
with:
|
with:
|
||||||
path: *path-apt-cache
|
path: *path-apt-cache
|
||||||
key: *key-apt-cache
|
key: *key-apt-cache
|
||||||
@@ -534,7 +534,7 @@ jobs:
|
|||||||
python --version
|
python --version
|
||||||
uv pip freeze >> pip_freeze.txt
|
uv pip freeze >> pip_freeze.txt
|
||||||
- name: Upload pip_freeze artifact
|
- name: Upload pip_freeze artifact
|
||||||
uses: &actions-upload-artifact actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: pip-freeze-${{ matrix.python-version }}
|
name: pip-freeze-${{ matrix.python-version }}
|
||||||
path: pip_freeze.txt
|
path: pip_freeze.txt
|
||||||
@@ -864,7 +864,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||||
- name: Download pytest_buckets
|
- name: Download pytest_buckets
|
||||||
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets
|
||||||
- &compile-english-translations
|
- &compile-english-translations
|
||||||
@@ -1188,7 +1188,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'true'
|
if: needs.info.outputs.test_full_suite == 'true'
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
flags: full-suite
|
flags: full-suite
|
||||||
@@ -1313,7 +1313,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'false'
|
if: needs.info.outputs.test_full_suite == 'false'
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||||
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ jobs:
|
|||||||
- name: Detect duplicates using AI
|
- name: Detect duplicates using AI
|
||||||
id: ai_detection
|
id: ai_detection
|
||||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||||
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
|
uses: actions/ai-inference@5022b33bc1431add9b2831934daf8147a2ad9331 # v2.0.2
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: Detect language using AI
|
- name: Detect language using AI
|
||||||
id: ai_language_detection
|
id: ai_language_detection
|
||||||
if: steps.detect_language.outputs.should_continue == 'true'
|
if: steps.detect_language.outputs.should_continue == 'true'
|
||||||
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
|
uses: actions/ai-inference@5022b33bc1431add9b2831934daf8147a2ad9331 # v2.0.2
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: "30"
|
issue-inactive-days: "30"
|
||||||
|
|||||||
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
# - No PRs marked as no-stale
|
# - No PRs marked as no-stale
|
||||||
# - No issues (-1)
|
# - No issues (-1)
|
||||||
- name: 60 days stale PRs policy
|
- name: 60 days stale PRs policy
|
||||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
# - No issues marked as no-stale or help-wanted
|
# - No issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: 90 days stale issues
|
- name: 90 days stale issues
|
||||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
# - No Issues marked as no-stale or help-wanted
|
# - No Issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: Needs more information stale issues policy
|
- name: Needs more information stale issues policy
|
||||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
only-labels: "needs-more-information"
|
only-labels: "needs-more-information"
|
||||||
|
|||||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
|
|||||||
8
.github/workflows/wheels.yml
vendored
8
.github/workflows/wheels.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- &checkout
|
- &checkout
|
||||||
name: Checkout the repository
|
name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
) > .env_file
|
) > .env_file
|
||||||
|
|
||||||
- name: Upload env_file
|
- name: Upload env_file
|
||||||
uses: &actions-upload-artifact actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
path: ./.env_file
|
path: ./.env_file
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
|
|
||||||
- &download-env-file
|
- &download-env-file
|
||||||
name: Download env_file
|
name: Download env_file
|
||||||
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ jobs:
|
|||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: &home-assistant-wheels home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
uses: &home-assistant-wheels home-assistant/wheels@6066c17a2a4aafcf7bdfeae01717f63adfcdba98 # 2025.11.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
|
|||||||
22
CODEOWNERS
generated
22
CODEOWNERS
generated
@@ -73,8 +73,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/airobot/ @mettolen
|
/tests/components/airobot/ @mettolen
|
||||||
/homeassistant/components/airos/ @CoMPaTech
|
/homeassistant/components/airos/ @CoMPaTech
|
||||||
/tests/components/airos/ @CoMPaTech
|
/tests/components/airos/ @CoMPaTech
|
||||||
/homeassistant/components/airpatrol/ @antondalgren
|
|
||||||
/tests/components/airpatrol/ @antondalgren
|
|
||||||
/homeassistant/components/airq/ @Sibgatulin @dl2080
|
/homeassistant/components/airq/ @Sibgatulin @dl2080
|
||||||
/tests/components/airq/ @Sibgatulin @dl2080
|
/tests/components/airq/ @Sibgatulin @dl2080
|
||||||
/homeassistant/components/airthings/ @danielhiversen @LaStrada
|
/homeassistant/components/airthings/ @danielhiversen @LaStrada
|
||||||
@@ -220,8 +218,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
||||||
/homeassistant/components/blebox/ @bbx-a @swistakm
|
/homeassistant/components/blebox/ @bbx-a @swistakm
|
||||||
/tests/components/blebox/ @bbx-a @swistakm
|
/tests/components/blebox/ @bbx-a @swistakm
|
||||||
/homeassistant/components/blink/ @fronzbot
|
/homeassistant/components/blink/ @fronzbot @mkmer
|
||||||
/tests/components/blink/ @fronzbot
|
/tests/components/blink/ @fronzbot @mkmer
|
||||||
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||||
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||||
/homeassistant/components/bluemaestro/ @bdraco
|
/homeassistant/components/bluemaestro/ @bdraco
|
||||||
@@ -308,8 +306,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/config/ @home-assistant/core
|
/tests/components/config/ @home-assistant/core
|
||||||
/homeassistant/components/configurator/ @home-assistant/core
|
/homeassistant/components/configurator/ @home-assistant/core
|
||||||
/tests/components/configurator/ @home-assistant/core
|
/tests/components/configurator/ @home-assistant/core
|
||||||
/homeassistant/components/control4/ @lawtancool @davidrecordon
|
/homeassistant/components/control4/ @lawtancool
|
||||||
/tests/components/control4/ @lawtancool @davidrecordon
|
/tests/components/control4/ @lawtancool
|
||||||
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/tests/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
/tests/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/homeassistant/components/cookidoo/ @miaucl
|
/homeassistant/components/cookidoo/ @miaucl
|
||||||
@@ -420,8 +418,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/efergy/ @tkdrob
|
/homeassistant/components/efergy/ @tkdrob
|
||||||
/tests/components/efergy/ @tkdrob
|
/tests/components/efergy/ @tkdrob
|
||||||
/homeassistant/components/egardia/ @jeroenterheerdt
|
/homeassistant/components/egardia/ @jeroenterheerdt
|
||||||
/homeassistant/components/egauge/ @neggert
|
|
||||||
/tests/components/egauge/ @neggert
|
|
||||||
/homeassistant/components/eheimdigital/ @autinerd
|
/homeassistant/components/eheimdigital/ @autinerd
|
||||||
/tests/components/eheimdigital/ @autinerd
|
/tests/components/eheimdigital/ @autinerd
|
||||||
/homeassistant/components/ekeybionyx/ @richardpolzer
|
/homeassistant/components/ekeybionyx/ @richardpolzer
|
||||||
@@ -464,7 +460,7 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/enigma2/ @autinerd
|
/tests/components/enigma2/ @autinerd
|
||||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
||||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
||||||
/homeassistant/components/entur_public_transport/ @hfurubotten @SanderBlom
|
/homeassistant/components/entur_public_transport/ @hfurubotten
|
||||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||||
/homeassistant/components/ephember/ @ttroy50 @roberty99
|
/homeassistant/components/ephember/ @ttroy50 @roberty99
|
||||||
@@ -543,8 +539,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/freebox/ @hacf-fr @Quentame
|
/tests/components/freebox/ @hacf-fr @Quentame
|
||||||
/homeassistant/components/freedompro/ @stefano055415
|
/homeassistant/components/freedompro/ @stefano055415
|
||||||
/tests/components/freedompro/ @stefano055415
|
/tests/components/freedompro/ @stefano055415
|
||||||
/homeassistant/components/fressnapf_tracker/ @eifinger
|
|
||||||
/tests/components/fressnapf_tracker/ @eifinger
|
|
||||||
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||||
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||||
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
||||||
@@ -575,8 +569,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/generic_hygrostat/ @Shulyaka
|
/tests/components/generic_hygrostat/ @Shulyaka
|
||||||
/homeassistant/components/geniushub/ @manzanotti
|
/homeassistant/components/geniushub/ @manzanotti
|
||||||
/tests/components/geniushub/ @manzanotti
|
/tests/components/geniushub/ @manzanotti
|
||||||
/homeassistant/components/gentex_homelink/ @niaexa @ryanjones-gentex
|
|
||||||
/tests/components/gentex_homelink/ @niaexa @ryanjones-gentex
|
|
||||||
/homeassistant/components/geo_json_events/ @exxamalte
|
/homeassistant/components/geo_json_events/ @exxamalte
|
||||||
/tests/components/geo_json_events/ @exxamalte
|
/tests/components/geo_json_events/ @exxamalte
|
||||||
/homeassistant/components/geo_location/ @home-assistant/core
|
/homeassistant/components/geo_location/ @home-assistant/core
|
||||||
@@ -665,7 +657,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/here_travel_time/ @eifinger
|
/homeassistant/components/here_travel_time/ @eifinger
|
||||||
/tests/components/here_travel_time/ @eifinger
|
/tests/components/here_travel_time/ @eifinger
|
||||||
/homeassistant/components/hikvision/ @mezz64
|
/homeassistant/components/hikvision/ @mezz64
|
||||||
/tests/components/hikvision/ @mezz64
|
|
||||||
/homeassistant/components/hikvisioncam/ @fbradyirl
|
/homeassistant/components/hikvisioncam/ @fbradyirl
|
||||||
/homeassistant/components/hisense_aehw4a1/ @bannhead
|
/homeassistant/components/hisense_aehw4a1/ @bannhead
|
||||||
/tests/components/hisense_aehw4a1/ @bannhead
|
/tests/components/hisense_aehw4a1/ @bannhead
|
||||||
@@ -1770,7 +1761,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/vilfo/ @ManneW
|
/homeassistant/components/vilfo/ @ManneW
|
||||||
/tests/components/vilfo/ @ManneW
|
/tests/components/vilfo/ @ManneW
|
||||||
/homeassistant/components/vivotek/ @HarlemSquirrel
|
/homeassistant/components/vivotek/ @HarlemSquirrel
|
||||||
/tests/components/vivotek/ @HarlemSquirrel
|
|
||||||
/homeassistant/components/vizio/ @raman325
|
/homeassistant/components/vizio/ @raman325
|
||||||
/tests/components/vizio/ @raman325
|
/tests/components/vizio/ @raman325
|
||||||
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
|
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
|
||||||
@@ -1810,8 +1800,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/weatherflow_cloud/ @jeeftor
|
/tests/components/weatherflow_cloud/ @jeeftor
|
||||||
/homeassistant/components/weatherkit/ @tjhorner
|
/homeassistant/components/weatherkit/ @tjhorner
|
||||||
/tests/components/weatherkit/ @tjhorner
|
/tests/components/weatherkit/ @tjhorner
|
||||||
/homeassistant/components/web_rtc/ @home-assistant/core
|
|
||||||
/tests/components/web_rtc/ @home-assistant/core
|
|
||||||
/homeassistant/components/webdav/ @jpbede
|
/homeassistant/components/webdav/ @jpbede
|
||||||
/tests/components/webdav/ @jpbede
|
/tests/components/webdav/ @jpbede
|
||||||
/homeassistant/components/webhook/ @home-assistant/core
|
/homeassistant/components/webhook/ @home-assistant/core
|
||||||
|
|||||||
2
Dockerfile
generated
2
Dockerfile
generated
@@ -30,7 +30,7 @@ RUN \
|
|||||||
# Verify go2rtc can be executed
|
# Verify go2rtc can be executed
|
||||||
go2rtc --version \
|
go2rtc --version \
|
||||||
# Install uv
|
# Install uv
|
||||||
&& pip3 install uv==0.9.17
|
&& pip3 install uv==0.9.6
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
|||||||
@@ -35,22 +35,25 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|||||||
|
|
||||||
USER vscode
|
USER vscode
|
||||||
|
|
||||||
|
COPY .python-version ./
|
||||||
|
RUN uv python install
|
||||||
|
|
||||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||||
RUN --mount=type=bind,source=.python-version,target=.python-version \
|
RUN uv venv $VIRTUAL_ENV
|
||||||
uv python install \
|
|
||||||
&& uv venv $VIRTUAL_ENV
|
|
||||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /tmp
|
||||||
|
|
||||||
# Setup hass-release
|
# Setup hass-release
|
||||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
|
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
|
||||||
&& uv pip install -e ~/hass-release/
|
&& uv pip install -e ~/hass-release/
|
||||||
|
|
||||||
# Install Python dependencies from requirements
|
# Install Python dependencies from requirements
|
||||||
RUN --mount=type=bind,source=requirements.txt,target=requirements.txt \
|
COPY requirements.txt ./
|
||||||
--mount=type=bind,source=homeassistant/package_constraints.txt,target=homeassistant/package_constraints.txt \
|
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
||||||
--mount=type=bind,source=requirements_test.txt,target=requirements_test.txt \
|
RUN uv pip install -r requirements.txt
|
||||||
--mount=type=bind,source=requirements_test_pre_commit.txt,target=requirements_test_pre_commit.txt \
|
COPY requirements_test.txt requirements_test_pre_commit.txt ./
|
||||||
uv pip install -r requirements.txt -r requirements_test.txt
|
RUN uv pip install -r requirements_test.txt
|
||||||
|
|
||||||
WORKDIR /workspaces
|
WORKDIR /workspaces
|
||||||
|
|
||||||
|
|||||||
@@ -624,16 +624,13 @@ async def async_enable_logging(
|
|||||||
|
|
||||||
if log_file is None:
|
if log_file is None:
|
||||||
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||||
if "SUPERVISOR" in os.environ and "HA_DUPLICATE_LOG_FILE" not in os.environ:
|
if "SUPERVISOR" in os.environ:
|
||||||
|
_LOGGER.info("Running in Supervisor, not logging to file")
|
||||||
# Rename the default log file if it exists, since previous versions created
|
# Rename the default log file if it exists, since previous versions created
|
||||||
# it even on Supervisor
|
# it even on Supervisor
|
||||||
def rename_old_file() -> None:
|
|
||||||
"""Rename old log file in executor."""
|
|
||||||
if os.path.isfile(default_log_path):
|
if os.path.isfile(default_log_path):
|
||||||
with contextlib.suppress(OSError):
|
with contextlib.suppress(OSError):
|
||||||
os.rename(default_log_path, f"{default_log_path}.old")
|
os.rename(default_log_path, f"{default_log_path}.old")
|
||||||
|
|
||||||
await hass.async_add_executor_job(rename_old_file)
|
|
||||||
err_log_path = None
|
err_log_path = None
|
||||||
else:
|
else:
|
||||||
err_log_path = default_log_path
|
err_log_path = default_log_path
|
||||||
@@ -1003,7 +1000,7 @@ class _WatchPendingSetups:
|
|||||||
# We log every LOG_SLOW_STARTUP_INTERVAL until all integrations are done
|
# We log every LOG_SLOW_STARTUP_INTERVAL until all integrations are done
|
||||||
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
|
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Waiting for integrations to complete setup: %s",
|
"Waiting on integrations to complete setup: %s",
|
||||||
self._setup_started,
|
self._setup_started,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ from actron_neo_api import (
|
|||||||
|
|
||||||
from homeassistant.const import CONF_API_TOKEN, Platform
|
from homeassistant.const import CONF_API_TOKEN, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
|
||||||
|
|
||||||
from .const import _LOGGER, DOMAIN
|
from .const import _LOGGER
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
ActronAirConfigEntry,
|
ActronAirConfigEntry,
|
||||||
ActronAirRuntimeData,
|
ActronAirRuntimeData,
|
||||||
@@ -30,13 +29,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
|
|||||||
try:
|
try:
|
||||||
systems = await api.get_ac_systems()
|
systems = await api.get_ac_systems()
|
||||||
await api.update_status()
|
await api.update_status()
|
||||||
except ActronAirAuthError as err:
|
except ActronAirAuthError:
|
||||||
raise ConfigEntryAuthFailed(
|
_LOGGER.error("Authentication error while setting up Actron Air integration")
|
||||||
translation_domain=DOMAIN,
|
raise
|
||||||
translation_key="auth_error",
|
|
||||||
) from err
|
|
||||||
except ActronAirAPIError as err:
|
except ActronAirAPIError as err:
|
||||||
raise ConfigEntryNotReady from err
|
_LOGGER.error("API error while setting up Actron Air integration: %s", err)
|
||||||
|
raise
|
||||||
|
|
||||||
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
|
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
|
||||||
for system in systems:
|
for system in systems:
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
"""Setup config flow for Actron Air integration."""
|
"""Setup config flow for Actron Air integration."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Mapping
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from actron_neo_api import ActronAirAPI, ActronAirAuthError
|
from actron_neo_api import ActronAirAPI, ActronAirAuthError
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_API_TOKEN
|
from homeassistant.const import CONF_API_TOKEN
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
@@ -96,16 +95,8 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
unique_id = str(user_data["id"])
|
unique_id = str(user_data["id"])
|
||||||
await self.async_set_unique_id(unique_id)
|
await self.async_set_unique_id(unique_id)
|
||||||
|
|
||||||
# Check if this is a reauth flow
|
|
||||||
if self.source == SOURCE_REAUTH:
|
|
||||||
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
self._get_reauth_entry(),
|
|
||||||
data_updates={CONF_API_TOKEN: self._api.refresh_token_value},
|
|
||||||
)
|
|
||||||
|
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=user_data["email"],
|
title=user_data["email"],
|
||||||
data={CONF_API_TOKEN: self._api.refresh_token_value},
|
data={CONF_API_TOKEN: self._api.refresh_token_value},
|
||||||
@@ -123,21 +114,6 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
del self.login_task
|
del self.login_task
|
||||||
return await self.async_step_user()
|
return await self.async_step_user()
|
||||||
|
|
||||||
async def async_step_reauth(
|
|
||||||
self, entry_data: Mapping[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle reauthentication request."""
|
|
||||||
return await self.async_step_reauth_confirm()
|
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Confirm reauth dialog."""
|
|
||||||
if user_input is not None:
|
|
||||||
return await self.async_step_user()
|
|
||||||
|
|
||||||
return self.async_show_form(step_id="reauth_confirm")
|
|
||||||
|
|
||||||
async def async_step_connection_error(
|
async def async_step_connection_error(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
|
|||||||
@@ -5,23 +5,16 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from actron_neo_api import (
|
from actron_neo_api import ActronAirACSystem, ActronAirAPI, ActronAirStatus
|
||||||
ActronAirACSystem,
|
|
||||||
ActronAirAPI,
|
|
||||||
ActronAirAuthError,
|
|
||||||
ActronAirStatus,
|
|
||||||
)
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import _LOGGER, DOMAIN
|
from .const import _LOGGER
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
STALE_DEVICE_TIMEOUT = timedelta(hours=24)
|
||||||
STALE_DEVICE_TIMEOUT = timedelta(minutes=5)
|
|
||||||
ERROR_NO_SYSTEMS_FOUND = "no_systems_found"
|
ERROR_NO_SYSTEMS_FOUND = "no_systems_found"
|
||||||
ERROR_UNKNOWN = "unknown_error"
|
ERROR_UNKNOWN = "unknown_error"
|
||||||
|
|
||||||
@@ -36,6 +29,9 @@ class ActronAirRuntimeData:
|
|||||||
|
|
||||||
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
|
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
|
||||||
|
|
||||||
|
AUTH_ERROR_THRESHOLD = 3
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
|
|
||||||
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
||||||
"""System coordinator for Actron Air integration."""
|
"""System coordinator for Actron Air integration."""
|
||||||
@@ -63,14 +59,7 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
|||||||
|
|
||||||
async def _async_update_data(self) -> ActronAirStatus:
|
async def _async_update_data(self) -> ActronAirStatus:
|
||||||
"""Fetch updates and merge incremental changes into the full state."""
|
"""Fetch updates and merge incremental changes into the full state."""
|
||||||
try:
|
|
||||||
await self.api.update_status()
|
await self.api.update_status()
|
||||||
except ActronAirAuthError as err:
|
|
||||||
raise ConfigEntryAuthFailed(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="auth_error",
|
|
||||||
) from err
|
|
||||||
|
|
||||||
self.status = self.api.state_manager.get_status(self.serial_number)
|
self.status = self.api.state_manager.get_status(self.serial_number)
|
||||||
self.last_seen = dt_util.utcnow()
|
self.last_seen = dt_util.utcnow()
|
||||||
return self.status
|
return self.status
|
||||||
|
|||||||
@@ -10,8 +10,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/actron_air",
|
"documentation": "https://www.home-assistant.io/integrations/actron_air",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["actron-neo-api==0.2.0"]
|
"requirements": ["actron-neo-api==0.1.87"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ rules:
|
|||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
parallel-updates: done
|
parallel-updates: done
|
||||||
reauthentication-flow: done
|
reauthentication-flow: todo
|
||||||
test-coverage: todo
|
test-coverage: todo
|
||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||||
"oauth2_error": "Failed to start authentication flow",
|
"oauth2_error": "Failed to start OAuth2 flow"
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
|
||||||
"wrong_account": "You must reauthenticate with the same Actron Air account that was originally configured."
|
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"oauth2_error": "Failed to start authentication flow. Please try again later."
|
"oauth2_error": "Failed to start OAuth2 flow. Please try again later."
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"wait_for_authorization": "To authenticate, open the following URL and login at Actron Air:\n{verification_uri}\nIf the code is not automatically copied, paste the following code to authorize the integration:\n\n```{user_code}```\n\n\nThe login attempt will time out after {expires_minutes} minutes."
|
"wait_for_authorization": "To authenticate, open the following URL and login at Actron Air:\n{verification_uri}\nIf the code is not automatically copied, paste the following code to authorize the integration:\n\n```{user_code}```\n\n\nThe login attempt will time out after {expires_minutes} minutes."
|
||||||
@@ -18,23 +16,14 @@
|
|||||||
"description": "Failed to connect to Actron Air. Please check your internet connection and try again.",
|
"description": "Failed to connect to Actron Air. Please check your internet connection and try again.",
|
||||||
"title": "Connection error"
|
"title": "Connection error"
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
|
||||||
"description": "Your Actron Air authentication has expired. Select continue to reauthenticate with your Actron Air account. You will be prompted to log in again to restore the connection.",
|
|
||||||
"title": "Authentication expired"
|
|
||||||
},
|
|
||||||
"timeout": {
|
"timeout": {
|
||||||
"data": {},
|
"data": {},
|
||||||
"description": "The authentication process timed out. Please try again.",
|
"description": "The authorization process timed out. Please try again.",
|
||||||
"title": "Authentication timeout"
|
"title": "Authorization timeout"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Actron Air Authentication"
|
"title": "Actron Air OAuth2 Authorization"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
"exceptions": {
|
|
||||||
"auth_error": {
|
|
||||||
"message": "Authentication failed, please reauthenticate"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@Bre77"],
|
"codeowners": ["@Bre77"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/advantage_air",
|
"documentation": "https://www.home-assistant.io/integrations/advantage_air",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["advantage_air"],
|
"loggers": ["advantage_air"],
|
||||||
"requirements": ["advantage-air==0.4.4"]
|
"requirements": ["advantage-air==0.4.4"]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@Noltari"],
|
"codeowners": ["@Noltari"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aemet_opendata"],
|
"loggers": ["aemet_opendata"],
|
||||||
"requirements": ["AEMET-OpenData==0.6.4"]
|
"requirements": ["AEMET-OpenData==0.6.4"]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": [],
|
"codeowners": [],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aftership",
|
"documentation": "https://www.home-assistant.io/integrations/aftership",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["pyaftership==21.11.0"]
|
"requirements": ["pyaftership==21.11.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@ispysoftware"],
|
"codeowners": ["@ispysoftware"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
|
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["agent"],
|
"loggers": ["agent"],
|
||||||
"requirements": ["agent-py==0.0.24"]
|
"requirements": ["agent-py==0.0.24"]
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
|
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
|
||||||
_validate_structure_fields,
|
_validate_structure_fields,
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
|
vol.Optional(ATTR_ATTACHMENTS): vol.All(
|
||||||
{"accept": ["*/*"], "multiple": True}
|
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -118,8 +118,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
vol.Required(ATTR_TASK_NAME): cv.string,
|
vol.Required(ATTR_TASK_NAME): cv.string,
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
||||||
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
||||||
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
|
vol.Optional(ATTR_ATTACHMENTS): vol.All(
|
||||||
{"accept": ["*/*"], "multiple": True}
|
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@asymworks"],
|
"codeowners": ["@asymworks"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airnow",
|
"documentation": "https://www.home-assistant.io/integrations/airnow",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyairnow"],
|
"loggers": ["pyairnow"],
|
||||||
"requirements": ["pyairnow==1.3.1"]
|
"requirements": ["pyairnow==1.3.1"]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
|
|
||||||
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
|
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.CLIMATE]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -175,56 +174,6 @@ class AirobotConfigFlow(BaseConfigFlow, domain=DOMAIN):
|
|||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_reauth(
|
|
||||||
self, entry_data: Mapping[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle reauthentication upon an API authentication error."""
|
|
||||||
return await self.async_step_reauth_confirm()
|
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Confirm reauthentication dialog."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
reauth_entry = self._get_reauth_entry()
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
# Combine existing data with new password
|
|
||||||
data = {
|
|
||||||
CONF_HOST: reauth_entry.data[CONF_HOST],
|
|
||||||
CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
|
|
||||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
await validate_input(self.hass, data)
|
|
||||||
except CannotConnect:
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
except InvalidAuth:
|
|
||||||
errors["base"] = "invalid_auth"
|
|
||||||
except Exception:
|
|
||||||
_LOGGER.exception("Unexpected exception")
|
|
||||||
errors["base"] = "unknown"
|
|
||||||
else:
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
reauth_entry,
|
|
||||||
data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]},
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="reauth_confirm",
|
|
||||||
data_schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_PASSWORD): str,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
description_placeholders={
|
|
||||||
"username": reauth_entry.data[CONF_USERNAME],
|
|
||||||
"host": reauth_entry.data[CONF_HOST],
|
|
||||||
},
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CannotConnect(HomeAssistantError):
|
class CannotConnect(HomeAssistantError):
|
||||||
"""Error to indicate we cannot connect."""
|
"""Error to indicate we cannot connect."""
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from pyairobotrest.exceptions import AirobotAuthError, AirobotConnectionError
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
@@ -54,15 +53,7 @@ class AirobotDataUpdateCoordinator(DataUpdateCoordinator[AirobotData]):
|
|||||||
try:
|
try:
|
||||||
status = await self.client.get_statuses()
|
status = await self.client.get_statuses()
|
||||||
settings = await self.client.get_settings()
|
settings = await self.client.get_settings()
|
||||||
except AirobotAuthError as err:
|
except (AirobotAuthError, AirobotConnectionError) as err:
|
||||||
raise ConfigEntryAuthFailed(
|
raise UpdateFailed(f"Failed to communicate with device: {err}") from err
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="authentication_failed",
|
|
||||||
) from err
|
|
||||||
except AirobotConnectionError as err:
|
|
||||||
raise UpdateFailed(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="connection_failed",
|
|
||||||
) from err
|
|
||||||
|
|
||||||
return AirobotData(status=status, settings=settings)
|
return AirobotData(status=status, settings=settings)
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
"""Diagnostics support for Airobot."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import asdict
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .coordinator import AirobotConfigEntry
|
|
||||||
|
|
||||||
TO_REDACT_CONFIG = [CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
|
||||||
hass: HomeAssistant, entry: AirobotConfigEntry
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Return diagnostics for a config entry."""
|
|
||||||
coordinator = entry.runtime_data
|
|
||||||
|
|
||||||
# Build device capabilities info
|
|
||||||
device_capabilities = None
|
|
||||||
if coordinator.data:
|
|
||||||
device_capabilities = {
|
|
||||||
"has_floor_sensor": coordinator.data.status.has_floor_sensor,
|
|
||||||
"has_co2_sensor": coordinator.data.status.has_co2_sensor,
|
|
||||||
"hw_version": coordinator.data.status.hw_version,
|
|
||||||
"fw_version": coordinator.data.status.fw_version,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"entry_data": async_redact_data(entry.data, TO_REDACT_CONFIG),
|
|
||||||
"device_capabilities": device_capabilities,
|
|
||||||
"status": asdict(coordinator.data.status) if coordinator.data else None,
|
|
||||||
"settings": asdict(coordinator.data.settings) if coordinator.data else None,
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,6 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyairobotrest"],
|
"loggers": ["pyairobotrest"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pyairobotrest==0.1.0"]
|
"requirements": ["pyairobotrest==0.1.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,17 +34,17 @@ rules:
|
|||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
parallel-updates: done
|
parallel-updates: done
|
||||||
reauthentication-flow: done
|
reauthentication-flow: todo
|
||||||
test-coverage: done
|
test-coverage: done
|
||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
devices: done
|
devices: done
|
||||||
diagnostics: done
|
diagnostics: todo
|
||||||
discovery-update-info: done
|
discovery-update-info: done
|
||||||
discovery: done
|
discovery: done
|
||||||
docs-data-update: done
|
docs-data-update: done
|
||||||
docs-examples: todo
|
docs-examples: todo
|
||||||
docs-known-limitations: done
|
docs-known-limitations: todo
|
||||||
docs-supported-devices: done
|
docs-supported-devices: done
|
||||||
docs-supported-functions: done
|
docs-supported-functions: done
|
||||||
docs-troubleshooting: done
|
docs-troubleshooting: done
|
||||||
@@ -54,8 +54,8 @@ rules:
|
|||||||
comment: Single device integration, no dynamic device discovery needed.
|
comment: Single device integration, no dynamic device discovery needed.
|
||||||
entity-category: done
|
entity-category: done
|
||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
entity-disabled-by-default: done
|
entity-disabled-by-default: todo
|
||||||
entity-translations: done
|
entity-translations: todo
|
||||||
exception-translations: done
|
exception-translations: done
|
||||||
icon-translations: todo
|
icon-translations: todo
|
||||||
reconfiguration-flow: todo
|
reconfiguration-flow: todo
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
"""Sensor platform for Airobot thermostat."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from pyairobotrest.models import ThermostatStatus
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
|
||||||
SensorDeviceClass,
|
|
||||||
SensorEntity,
|
|
||||||
SensorEntityDescription,
|
|
||||||
SensorStateClass,
|
|
||||||
)
|
|
||||||
from homeassistant.const import (
|
|
||||||
CONCENTRATION_PARTS_PER_MILLION,
|
|
||||||
PERCENTAGE,
|
|
||||||
EntityCategory,
|
|
||||||
UnitOfTemperature,
|
|
||||||
UnitOfTime,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
from homeassistant.helpers.typing import StateType
|
|
||||||
from homeassistant.util.dt import utcnow
|
|
||||||
from homeassistant.util.variance import ignore_variance
|
|
||||||
|
|
||||||
from . import AirobotConfigEntry
|
|
||||||
from .entity import AirobotEntity
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class AirobotSensorEntityDescription(SensorEntityDescription):
|
|
||||||
"""Describes Airobot sensor entity."""
|
|
||||||
|
|
||||||
value_fn: Callable[[ThermostatStatus], StateType | datetime]
|
|
||||||
supported_fn: Callable[[ThermostatStatus], bool] = lambda _: True
|
|
||||||
|
|
||||||
|
|
||||||
uptime_to_stable_datetime = ignore_variance(
|
|
||||||
lambda value: utcnow().replace(microsecond=0) - timedelta(seconds=value),
|
|
||||||
timedelta(minutes=2),
|
|
||||||
)
|
|
||||||
|
|
||||||
SENSOR_TYPES: tuple[AirobotSensorEntityDescription, ...] = (
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="air_temperature",
|
|
||||||
translation_key="air_temperature",
|
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
value_fn=lambda status: status.temp_air,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="humidity",
|
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
value_fn=lambda status: status.hum_air,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="floor_temperature",
|
|
||||||
translation_key="floor_temperature",
|
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
value_fn=lambda status: status.temp_floor,
|
|
||||||
supported_fn=lambda status: status.has_floor_sensor,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="co2",
|
|
||||||
device_class=SensorDeviceClass.CO2,
|
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
value_fn=lambda status: status.co2,
|
|
||||||
supported_fn=lambda status: status.has_co2_sensor,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="air_quality_index",
|
|
||||||
device_class=SensorDeviceClass.AQI,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
value_fn=lambda status: status.aqi,
|
|
||||||
supported_fn=lambda status: status.has_co2_sensor,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="heating_uptime",
|
|
||||||
translation_key="heating_uptime",
|
|
||||||
device_class=SensorDeviceClass.DURATION,
|
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
|
||||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
value_fn=lambda status: status.heating_uptime,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="errors",
|
|
||||||
translation_key="errors",
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
value_fn=lambda status: status.errors,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="device_uptime",
|
|
||||||
translation_key="device_uptime",
|
|
||||||
device_class=SensorDeviceClass.TIMESTAMP,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
value_fn=lambda status: uptime_to_stable_datetime(status.device_uptime),
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: AirobotConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up Airobot sensor platform."""
|
|
||||||
coordinator = entry.runtime_data
|
|
||||||
async_add_entities(
|
|
||||||
AirobotSensor(coordinator, description)
|
|
||||||
for description in SENSOR_TYPES
|
|
||||||
if description.supported_fn(coordinator.data.status)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AirobotSensor(AirobotEntity, SensorEntity):
|
|
||||||
"""Representation of an Airobot sensor."""
|
|
||||||
|
|
||||||
entity_description: AirobotSensorEntityDescription
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator,
|
|
||||||
description: AirobotSensorEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the sensor."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self.entity_description = description
|
|
||||||
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> StateType | datetime:
|
|
||||||
"""Return the state of the sensor."""
|
|
||||||
return self.entity_description.value_fn(self.coordinator.data.status)
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
@@ -15,24 +14,15 @@
|
|||||||
"password": "[%key:common::config_flow::data::password%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"password": "[%key:component::airobot::config::step::user::data_description::password%]"
|
"password": "The thermostat password."
|
||||||
},
|
},
|
||||||
"description": "Airobot thermostat {device_id} discovered at {host}. Enter the password to complete setup. Find the password in the thermostat settings menu under Connectivity → Mobile app."
|
"description": "Airobot thermostat {device_id} discovered at {host}. Enter the password to complete setup. Find the password in the thermostat settings menu under Connectivity → Mobile app."
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
|
||||||
"data": {
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"password": "[%key:component::airobot::config::step::user::data_description::password%]"
|
|
||||||
},
|
|
||||||
"description": "The authentication for Airobot thermostat at {host} (Device ID: {username}) has expired. Please enter the password to reauthenticate. Find the password in the thermostat settings menu under Connectivity → Mobile app."
|
|
||||||
},
|
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"username": "Device ID"
|
"username": "[%key:common::config_flow::data::username%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"host": "The hostname or IP address of your Airobot thermostat.",
|
"host": "The hostname or IP address of your Airobot thermostat.",
|
||||||
@@ -43,32 +33,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
|
||||||
"sensor": {
|
|
||||||
"air_temperature": {
|
|
||||||
"name": "Air temperature"
|
|
||||||
},
|
|
||||||
"device_uptime": {
|
|
||||||
"name": "Device uptime"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"name": "Error count"
|
|
||||||
},
|
|
||||||
"floor_temperature": {
|
|
||||||
"name": "Floor temperature"
|
|
||||||
},
|
|
||||||
"heating_uptime": {
|
|
||||||
"name": "Heating uptime"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
"authentication_failed": {
|
|
||||||
"message": "Authentication failed, please reauthenticate."
|
|
||||||
},
|
|
||||||
"connection_failed": {
|
|
||||||
"message": "Failed to communicate with device."
|
|
||||||
},
|
|
||||||
"set_preset_mode_failed": {
|
"set_preset_mode_failed": {
|
||||||
"message": "Failed to set preset mode to {preset_mode}."
|
"message": "Failed to set preset mode to {preset_mode}."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
"""The AirPatrol integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .const import PLATFORMS
|
|
||||||
from .coordinator import AirPatrolConfigEntry, AirPatrolDataUpdateCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirPatrolConfigEntry) -> bool:
|
|
||||||
"""Set up AirPatrol from a config entry."""
|
|
||||||
coordinator = AirPatrolDataUpdateCoordinator(hass, entry)
|
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
|
||||||
entry.runtime_data = coordinator
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: AirPatrolConfigEntry) -> bool:
|
|
||||||
"""Unload a config entry."""
|
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
"""Climate platform for AirPatrol integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
|
||||||
FAN_AUTO,
|
|
||||||
FAN_HIGH,
|
|
||||||
FAN_LOW,
|
|
||||||
SWING_OFF,
|
|
||||||
SWING_ON,
|
|
||||||
ClimateEntity,
|
|
||||||
ClimateEntityFeature,
|
|
||||||
HVACMode,
|
|
||||||
)
|
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
from . import AirPatrolConfigEntry
|
|
||||||
from .coordinator import AirPatrolDataUpdateCoordinator
|
|
||||||
from .entity import AirPatrolEntity
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
|
||||||
|
|
||||||
AP_TO_HA_HVAC_MODES = {
|
|
||||||
"heat": HVACMode.HEAT,
|
|
||||||
"cool": HVACMode.COOL,
|
|
||||||
"off": HVACMode.OFF,
|
|
||||||
}
|
|
||||||
HA_TO_AP_HVAC_MODES = {value: key for key, value in AP_TO_HA_HVAC_MODES.items()}
|
|
||||||
|
|
||||||
AP_TO_HA_FAN_MODES = {
|
|
||||||
"min": FAN_LOW,
|
|
||||||
"max": FAN_HIGH,
|
|
||||||
"auto": FAN_AUTO,
|
|
||||||
}
|
|
||||||
HA_TO_AP_FAN_MODES = {value: key for key, value in AP_TO_HA_FAN_MODES.items()}
|
|
||||||
|
|
||||||
AP_TO_HA_SWING_MODES = {
|
|
||||||
"on": SWING_ON,
|
|
||||||
"off": SWING_OFF,
|
|
||||||
}
|
|
||||||
HA_TO_AP_SWING_MODES = {value: key for key, value in AP_TO_HA_SWING_MODES.items()}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
config_entry: AirPatrolConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up AirPatrol climate entities."""
|
|
||||||
coordinator = config_entry.runtime_data
|
|
||||||
units = coordinator.data
|
|
||||||
|
|
||||||
async_add_entities(
|
|
||||||
AirPatrolClimate(coordinator, unit_id)
|
|
||||||
for unit_id, unit in units.items()
|
|
||||||
if "climate" in unit
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AirPatrolClimate(AirPatrolEntity, ClimateEntity):
|
|
||||||
"""AirPatrol climate entity."""
|
|
||||||
|
|
||||||
_attr_name = None
|
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
|
||||||
_attr_supported_features = (
|
|
||||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
|
||||||
| ClimateEntityFeature.FAN_MODE
|
|
||||||
| ClimateEntityFeature.SWING_MODE
|
|
||||||
| ClimateEntityFeature.TURN_OFF
|
|
||||||
| ClimateEntityFeature.TURN_ON
|
|
||||||
)
|
|
||||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
|
|
||||||
_attr_fan_modes = [FAN_LOW, FAN_HIGH, FAN_AUTO]
|
|
||||||
_attr_swing_modes = [SWING_ON, SWING_OFF]
|
|
||||||
_attr_min_temp = 16.0
|
|
||||||
_attr_max_temp = 30.0
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: AirPatrolDataUpdateCoordinator,
|
|
||||||
unit_id: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the climate entity."""
|
|
||||||
super().__init__(coordinator, unit_id)
|
|
||||||
self._attr_unique_id = f"{coordinator.config_entry.unique_id}-{unit_id}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def climate_data(self) -> dict[str, Any]:
|
|
||||||
"""Return the climate data."""
|
|
||||||
return self.device_data.get("climate") or {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def params(self) -> dict[str, Any]:
|
|
||||||
"""Return the current parameters for the climate entity."""
|
|
||||||
return self.climate_data.get("ParametersData") or {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return super().available and bool(self.climate_data)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_humidity(self) -> float | None:
|
|
||||||
"""Return the current humidity."""
|
|
||||||
if humidity := self.climate_data.get("RoomHumidity"):
|
|
||||||
return float(humidity)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_temperature(self) -> float | None:
|
|
||||||
"""Return the current temperature."""
|
|
||||||
if temp := self.climate_data.get("RoomTemp"):
|
|
||||||
return float(temp)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_temperature(self) -> float | None:
|
|
||||||
"""Return the target temperature."""
|
|
||||||
if temp := self.params.get("PumpTemp"):
|
|
||||||
return float(temp)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hvac_mode(self) -> HVACMode | None:
|
|
||||||
"""Return the current HVAC mode."""
|
|
||||||
pump_power = self.params.get("PumpPower")
|
|
||||||
pump_mode = self.params.get("PumpMode")
|
|
||||||
|
|
||||||
if pump_power and pump_power == "on" and pump_mode:
|
|
||||||
return AP_TO_HA_HVAC_MODES.get(pump_mode)
|
|
||||||
return HVACMode.OFF
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fan_mode(self) -> str | None:
|
|
||||||
"""Return the current fan mode."""
|
|
||||||
fan_speed = self.params.get("FanSpeed")
|
|
||||||
if fan_speed:
|
|
||||||
return AP_TO_HA_FAN_MODES.get(fan_speed)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def swing_mode(self) -> str | None:
|
|
||||||
"""Return the current swing mode."""
|
|
||||||
swing = self.params.get("Swing")
|
|
||||||
if swing:
|
|
||||||
return AP_TO_HA_SWING_MODES.get(swing)
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
|
||||||
"""Set new target temperature."""
|
|
||||||
params = self.params.copy()
|
|
||||||
|
|
||||||
if ATTR_TEMPERATURE in kwargs:
|
|
||||||
temp = kwargs[ATTR_TEMPERATURE]
|
|
||||||
params["PumpTemp"] = f"{temp:.3f}"
|
|
||||||
|
|
||||||
await self._async_set_params(params)
|
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
|
||||||
"""Set new target hvac mode."""
|
|
||||||
params = self.params.copy()
|
|
||||||
|
|
||||||
if hvac_mode == HVACMode.OFF:
|
|
||||||
params["PumpPower"] = "off"
|
|
||||||
else:
|
|
||||||
params["PumpPower"] = "on"
|
|
||||||
params["PumpMode"] = HA_TO_AP_HVAC_MODES.get(hvac_mode)
|
|
||||||
|
|
||||||
await self._async_set_params(params)
|
|
||||||
|
|
||||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
|
||||||
"""Set new target fan mode."""
|
|
||||||
params = self.params.copy()
|
|
||||||
params["FanSpeed"] = HA_TO_AP_FAN_MODES.get(fan_mode)
|
|
||||||
|
|
||||||
await self._async_set_params(params)
|
|
||||||
|
|
||||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
|
||||||
"""Set new target swing mode."""
|
|
||||||
params = self.params.copy()
|
|
||||||
params["Swing"] = HA_TO_AP_SWING_MODES.get(swing_mode)
|
|
||||||
|
|
||||||
await self._async_set_params(params)
|
|
||||||
|
|
||||||
async def async_turn_on(self) -> None:
|
|
||||||
"""Turn the entity on."""
|
|
||||||
params = self.params.copy()
|
|
||||||
if mode := AP_TO_HA_HVAC_MODES.get(params["PumpMode"]):
|
|
||||||
await self.async_set_hvac_mode(mode)
|
|
||||||
|
|
||||||
async def async_turn_off(self) -> None:
|
|
||||||
"""Turn the entity off."""
|
|
||||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
|
||||||
|
|
||||||
async def _async_set_params(self, params: dict[str, Any]) -> None:
|
|
||||||
"""Set the unit to dry mode."""
|
|
||||||
new_climate_data = self.climate_data.copy()
|
|
||||||
new_climate_data["ParametersData"] = params
|
|
||||||
|
|
||||||
await self.coordinator.api.set_unit_climate_data(
|
|
||||||
self._unit_id, new_climate_data
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.coordinator.async_request_refresh()
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
"""Config flow for the AirPatrol integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from airpatrol.api import AirPatrolAPI, AirPatrolAuthenticationError, AirPatrolError
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.helpers.selector import (
|
|
||||||
TextSelector,
|
|
||||||
TextSelectorConfig,
|
|
||||||
TextSelectorType,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_EMAIL): TextSelector(
|
|
||||||
TextSelectorConfig(
|
|
||||||
type=TextSelectorType.EMAIL,
|
|
||||||
autocomplete="email",
|
|
||||||
)
|
|
||||||
),
|
|
||||||
vol.Required(CONF_PASSWORD): TextSelector(
|
|
||||||
TextSelectorConfig(
|
|
||||||
type=TextSelectorType.PASSWORD,
|
|
||||||
autocomplete="current-password",
|
|
||||||
)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def validate_api(
|
|
||||||
hass: HomeAssistant, user_input: dict[str, str]
|
|
||||||
) -> tuple[str | None, str | None, dict[str, str]]:
|
|
||||||
"""Validate the API connection."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
session = async_get_clientsession(hass)
|
|
||||||
access_token = None
|
|
||||||
unique_id = None
|
|
||||||
try:
|
|
||||||
api = await AirPatrolAPI.authenticate(
|
|
||||||
session, user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
|
||||||
)
|
|
||||||
except AirPatrolAuthenticationError:
|
|
||||||
errors["base"] = "invalid_auth"
|
|
||||||
except AirPatrolError:
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
else:
|
|
||||||
access_token = api.get_access_token()
|
|
||||||
unique_id = api.get_unique_id()
|
|
||||||
|
|
||||||
return (access_token, unique_id, errors)
|
|
||||||
|
|
||||||
|
|
||||||
class AirPatrolConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
||||||
"""Handle a config flow for AirPatrol."""
|
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
|
|
||||||
async def async_step_user(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle the initial step."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
if user_input is not None:
|
|
||||||
access_token, unique_id, errors = await validate_api(self.hass, user_input)
|
|
||||||
if access_token and unique_id:
|
|
||||||
user_input[CONF_ACCESS_TOKEN] = access_token
|
|
||||||
await self.async_set_unique_id(unique_id)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=user_input[CONF_EMAIL], data=user_input
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_reauth(
|
|
||||||
self, user_input: Mapping[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle reauthentication with new credentials."""
|
|
||||||
return await self.async_step_reauth_confirm()
|
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle reauthentication confirmation."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
|
|
||||||
if user_input:
|
|
||||||
access_token, unique_id, errors = await validate_api(self.hass, user_input)
|
|
||||||
if access_token and unique_id:
|
|
||||||
await self.async_set_unique_id(unique_id)
|
|
||||||
self._abort_if_unique_id_mismatch()
|
|
||||||
user_input[CONF_ACCESS_TOKEN] = access_token
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
self._get_reauth_entry(), data_updates=user_input
|
|
||||||
)
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="reauth_confirm", data_schema=DATA_SCHEMA, errors=errors
|
|
||||||
)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
"""Constants for the AirPatrol integration."""
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from airpatrol.api import AirPatrolAuthenticationError, AirPatrolError
|
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
|
||||||
|
|
||||||
DOMAIN = "airpatrol"
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__package__)
|
|
||||||
PLATFORMS = [Platform.CLIMATE]
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=1)
|
|
||||||
|
|
||||||
AIRPATROL_ERRORS = (AirPatrolAuthenticationError, AirPatrolError)
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
"""Data update coordinator for AirPatrol."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from airpatrol.api import AirPatrolAPI, AirPatrolAuthenticationError, AirPatrolError
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
|
||||||
|
|
||||||
type AirPatrolConfigEntry = ConfigEntry[AirPatrolDataUpdateCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
class AirPatrolDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
|
||||||
"""Class to manage fetching AirPatrol data."""
|
|
||||||
|
|
||||||
config_entry: AirPatrolConfigEntry
|
|
||||||
api: AirPatrolAPI
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config_entry: AirPatrolConfigEntry) -> None:
|
|
||||||
"""Initialize."""
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
hass,
|
|
||||||
LOGGER,
|
|
||||||
name=f"{DOMAIN.capitalize()} {config_entry.title}",
|
|
||||||
update_interval=SCAN_INTERVAL,
|
|
||||||
config_entry=config_entry,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _async_setup(self) -> None:
|
|
||||||
try:
|
|
||||||
await self._setup_client()
|
|
||||||
except AirPatrolError as api_err:
|
|
||||||
raise UpdateFailed(
|
|
||||||
f"Error communicating with AirPatrol API: {api_err}"
|
|
||||||
) from api_err
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
|
||||||
"""Update unit data from AirPatrol API."""
|
|
||||||
return {unit_data["unit_id"]: unit_data for unit_data in await self._get_data()}
|
|
||||||
|
|
||||||
async def _get_data(self, retry: bool = False) -> list[dict[str, Any]]:
|
|
||||||
"""Fetch data from API."""
|
|
||||||
try:
|
|
||||||
return await self.api.get_data()
|
|
||||||
except AirPatrolAuthenticationError as auth_err:
|
|
||||||
if retry:
|
|
||||||
raise ConfigEntryAuthFailed(
|
|
||||||
"Authentication with AirPatrol failed"
|
|
||||||
) from auth_err
|
|
||||||
await self._update_token()
|
|
||||||
return await self._get_data(retry=True)
|
|
||||||
except AirPatrolError as err:
|
|
||||||
raise UpdateFailed(
|
|
||||||
f"Error communicating with AirPatrol API: {err}"
|
|
||||||
) from err
|
|
||||||
|
|
||||||
async def _update_token(self) -> None:
|
|
||||||
"""Refresh the AirPatrol API client and update the access token."""
|
|
||||||
session = async_get_clientsession(self.hass)
|
|
||||||
try:
|
|
||||||
self.api = await AirPatrolAPI.authenticate(
|
|
||||||
session,
|
|
||||||
self.config_entry.data[CONF_EMAIL],
|
|
||||||
self.config_entry.data[CONF_PASSWORD],
|
|
||||||
)
|
|
||||||
except AirPatrolAuthenticationError as auth_err:
|
|
||||||
raise ConfigEntryAuthFailed(
|
|
||||||
"Authentication with AirPatrol failed"
|
|
||||||
) from auth_err
|
|
||||||
|
|
||||||
self.hass.config_entries.async_update_entry(
|
|
||||||
self.config_entry,
|
|
||||||
data={
|
|
||||||
**self.config_entry.data,
|
|
||||||
CONF_ACCESS_TOKEN: self.api.get_access_token(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _setup_client(self) -> None:
|
|
||||||
"""Set up the AirPatrol API client from stored access_token."""
|
|
||||||
session = async_get_clientsession(self.hass)
|
|
||||||
api = AirPatrolAPI(
|
|
||||||
session,
|
|
||||||
self.config_entry.data[CONF_ACCESS_TOKEN],
|
|
||||||
self.config_entry.unique_id,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await api.get_data()
|
|
||||||
except AirPatrolAuthenticationError:
|
|
||||||
await self._update_token()
|
|
||||||
self.api = api
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
"""Base entity for AirPatrol integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .coordinator import AirPatrolDataUpdateCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
class AirPatrolEntity(CoordinatorEntity[AirPatrolDataUpdateCoordinator]):
|
|
||||||
"""Base entity for AirPatrol devices."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: AirPatrolDataUpdateCoordinator,
|
|
||||||
unit_id: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the AirPatrol entity."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._unit_id = unit_id
|
|
||||||
device = coordinator.data[unit_id]
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, unit_id)},
|
|
||||||
name=device["name"],
|
|
||||||
manufacturer=device["manufacturer"],
|
|
||||||
model=device["model"],
|
|
||||||
serial_number=device["hwid"],
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_data(self) -> dict[str, Any]:
|
|
||||||
"""Return the device data."""
|
|
||||||
return self.coordinator.data[self._unit_id]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return super().available and self._unit_id in self.coordinator.data
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "airpatrol",
|
|
||||||
"name": "AirPatrol",
|
|
||||||
"codeowners": ["@antondalgren"],
|
|
||||||
"config_flow": true,
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airpatrol",
|
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "cloud_polling",
|
|
||||||
"quality_scale": "bronze",
|
|
||||||
"requirements": ["airpatrol==0.1.0"]
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
rules:
|
|
||||||
# Bronze
|
|
||||||
action-setup: done
|
|
||||||
appropriate-polling: done
|
|
||||||
brands: done
|
|
||||||
common-modules: done
|
|
||||||
config-flow-test-coverage: done
|
|
||||||
config-flow: done
|
|
||||||
dependency-transparency: done
|
|
||||||
docs-actions:
|
|
||||||
status: exempt
|
|
||||||
comment: Integration does not provide custom actions
|
|
||||||
docs-high-level-description: done
|
|
||||||
docs-installation-instructions: done
|
|
||||||
docs-removal-instructions: done
|
|
||||||
entity-event-setup:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
Entities doesn't subscribe to events.
|
|
||||||
entity-unique-id: done
|
|
||||||
has-entity-name: done
|
|
||||||
runtime-data: done
|
|
||||||
test-before-configure: done
|
|
||||||
test-before-setup: done
|
|
||||||
unique-config-entry: done
|
|
||||||
|
|
||||||
# Silver
|
|
||||||
action-exceptions: done
|
|
||||||
config-entry-unloading: done
|
|
||||||
docs-configuration-parameters: done
|
|
||||||
docs-installation-parameters: done
|
|
||||||
entity-unavailable: done
|
|
||||||
integration-owner: done
|
|
||||||
log-when-unavailable: todo
|
|
||||||
parallel-updates: done
|
|
||||||
reauthentication-flow: done
|
|
||||||
test-coverage: done
|
|
||||||
|
|
||||||
# Gold
|
|
||||||
devices: done
|
|
||||||
diagnostics: todo
|
|
||||||
discovery-update-info: todo
|
|
||||||
discovery: todo
|
|
||||||
docs-data-update: todo
|
|
||||||
docs-examples: todo
|
|
||||||
docs-known-limitations: todo
|
|
||||||
docs-supported-devices: todo
|
|
||||||
docs-supported-functions: todo
|
|
||||||
docs-troubleshooting: todo
|
|
||||||
docs-use-cases: todo
|
|
||||||
dynamic-devices: todo
|
|
||||||
entity-category: done
|
|
||||||
entity-device-class: done
|
|
||||||
entity-disabled-by-default: todo
|
|
||||||
entity-translations: done
|
|
||||||
exception-translations: todo
|
|
||||||
icon-translations: todo
|
|
||||||
reconfiguration-flow: todo
|
|
||||||
repair-issues: todo
|
|
||||||
stale-devices: todo
|
|
||||||
|
|
||||||
# Platinum
|
|
||||||
async-dependency: todo
|
|
||||||
inject-websession: todo
|
|
||||||
strict-typing: todo
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
|
||||||
"unique_id_mismatch": "Login credentials do not match the configured account"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
|
||||||
},
|
|
||||||
"step": {
|
|
||||||
"reauth_confirm": {
|
|
||||||
"data": {
|
|
||||||
"email": "[%key:common::config_flow::data::email%]",
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"email": "[%key:component::airpatrol::config::step::user::data_description::email%]",
|
|
||||||
"password": "[%key:component::airpatrol::config::step::user::data_description::password%]"
|
|
||||||
},
|
|
||||||
"description": "Reauthenticate with AirPatrol"
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"data": {
|
|
||||||
"email": "[%key:common::config_flow::data::email%]",
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"email": "Your AirPatrol email address",
|
|
||||||
"password": "Your AirPatrol password"
|
|
||||||
},
|
|
||||||
"description": "Connect to AirPatrol"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airthings",
|
"documentation": "https://www.home-assistant.io/integrations/airthings",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["airthings"],
|
"loggers": ["airthings"],
|
||||||
"requirements": ["airthings-cloud==0.2.0"]
|
"requirements": ["airthings-cloud==0.2.0"]
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["airthings-ble==1.2.0"]
|
"requirements": ["airthings-ble==1.2.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@samsinnamon"],
|
"codeowners": ["@samsinnamon"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airtouch4",
|
"documentation": "https://www.home-assistant.io/integrations/airtouch4",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["airtouch4pyapi"],
|
"loggers": ["airtouch4pyapi"],
|
||||||
"requirements": ["airtouch4pyapi==1.0.5"]
|
"requirements": ["airtouch4pyapi==1.0.5"]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@danzel"],
|
"codeowners": ["@danzel"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["airtouch5py"],
|
"loggers": ["airtouch5py"],
|
||||||
"requirements": ["airtouch5py==0.3.0"]
|
"requirements": ["airtouch5py==0.3.0"]
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairzone"],
|
"loggers": ["aioairzone"],
|
||||||
"requirements": ["aioairzone==1.0.4"]
|
"requirements": ["aioairzone==1.0.2"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@Noltari"],
|
"codeowners": ["@Noltari"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioairzone_cloud"],
|
"loggers": ["aioairzone_cloud"],
|
||||||
"requirements": ["aioairzone-cloud==0.7.2"]
|
"requirements": ["aioairzone-cloud==0.7.2"]
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity import get_supported_features
|
from homeassistant.helpers.entity import get_supported_features
|
||||||
from homeassistant.helpers.trigger import (
|
from homeassistant.helpers.trigger import (
|
||||||
EntityTargetStateTriggerBase,
|
EntityStateTriggerBase,
|
||||||
Trigger,
|
Trigger,
|
||||||
make_entity_target_state_trigger,
|
make_conditional_entity_state_trigger,
|
||||||
make_entity_transition_trigger,
|
make_entity_state_trigger,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
|
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
|
||||||
@@ -21,7 +21,7 @@ def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase):
|
class EntityStateTriggerRequiredFeatures(EntityStateTriggerBase):
|
||||||
"""Trigger for entity state changes."""
|
"""Trigger for entity state changes."""
|
||||||
|
|
||||||
_required_features: int
|
_required_features: int
|
||||||
@@ -38,7 +38,7 @@ class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase):
|
|||||||
|
|
||||||
def make_entity_state_trigger_required_features(
|
def make_entity_state_trigger_required_features(
|
||||||
domain: str, to_state: str, required_features: int
|
domain: str, to_state: str, required_features: int
|
||||||
) -> type[EntityTargetStateTriggerBase]:
|
) -> type[EntityStateTriggerBase]:
|
||||||
"""Create an entity state trigger class."""
|
"""Create an entity state trigger class."""
|
||||||
|
|
||||||
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
||||||
@@ -52,7 +52,7 @@ def make_entity_state_trigger_required_features(
|
|||||||
|
|
||||||
|
|
||||||
TRIGGERS: dict[str, type[Trigger]] = {
|
TRIGGERS: dict[str, type[Trigger]] = {
|
||||||
"armed": make_entity_transition_trigger(
|
"armed": make_conditional_entity_state_trigger(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
from_states={
|
from_states={
|
||||||
AlarmControlPanelState.ARMING,
|
AlarmControlPanelState.ARMING,
|
||||||
@@ -89,12 +89,8 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
|||||||
AlarmControlPanelState.ARMED_VACATION,
|
AlarmControlPanelState.ARMED_VACATION,
|
||||||
AlarmControlPanelEntityFeature.ARM_VACATION,
|
AlarmControlPanelEntityFeature.ARM_VACATION,
|
||||||
),
|
),
|
||||||
"disarmed": make_entity_target_state_trigger(
|
"disarmed": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.DISARMED),
|
||||||
DOMAIN, AlarmControlPanelState.DISARMED
|
"triggered": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.TRIGGERED),
|
||||||
),
|
|
||||||
"triggered": make_entity_target_state_trigger(
|
|
||||||
DOMAIN, AlarmControlPanelState.TRIGGERED
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@madpilot"],
|
"codeowners": ["@madpilot"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
|
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["amberelectric"],
|
"loggers": ["amberelectric"],
|
||||||
"requirements": ["amberelectric==2.0.12"]
|
"requirements": ["amberelectric==2.0.12"]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@engrbm87"],
|
"codeowners": ["@engrbm87"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["pydroid-ipcam==3.0.0"]
|
"requirements": ["pydroid-ipcam==3.0.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ async def async_setup_entry(
|
|||||||
cookie_jar=CookieJar(quote_cookie=False),
|
cookie_jar=CookieJar(quote_cookie=False),
|
||||||
),
|
),
|
||||||
refresh_token=entry.data[CONF_ACCESS_TOKEN],
|
refresh_token=entry.data[CONF_ACCESS_TOKEN],
|
||||||
|
account_number=entry.data[CONF_ACCOUNT_NUMBER],
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await auth.send_refresh_request()
|
await auth.send_refresh_request()
|
||||||
@@ -48,7 +49,7 @@ async def async_setup_entry(
|
|||||||
_aw = AnglianWater(authenticator=auth)
|
_aw = AnglianWater(authenticator=auth)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await _aw.validate_smart_meter(entry.data[CONF_ACCOUNT_NUMBER])
|
await _aw.validate_smart_meter()
|
||||||
except SmartMeterUnavailableError as err:
|
except SmartMeterUnavailableError as err:
|
||||||
raise ConfigEntryError(
|
raise ConfigEntryError(
|
||||||
translation_domain=DOMAIN, translation_key="smart_meter_unavailable"
|
translation_domain=DOMAIN, translation_key="smart_meter_unavailable"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import Any
|
|||||||
|
|
||||||
from aiohttp import CookieJar
|
from aiohttp import CookieJar
|
||||||
from pyanglianwater import AnglianWater
|
from pyanglianwater import AnglianWater
|
||||||
from pyanglianwater.auth import MSOB2CAuth
|
from pyanglianwater.auth import BaseAuth, MSOB2CAuth
|
||||||
from pyanglianwater.exceptions import (
|
from pyanglianwater.exceptions import (
|
||||||
InvalidAccountIdError,
|
InvalidAccountIdError,
|
||||||
SelfAssertedError,
|
SelfAssertedError,
|
||||||
@@ -35,9 +35,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def validate_credentials(
|
async def validate_credentials(auth: MSOB2CAuth) -> str | MSOB2CAuth:
|
||||||
auth: MSOB2CAuth, account_number: str
|
|
||||||
) -> str | MSOB2CAuth:
|
|
||||||
"""Validate the provided credentials."""
|
"""Validate the provided credentials."""
|
||||||
try:
|
try:
|
||||||
await auth.send_login_request()
|
await auth.send_login_request()
|
||||||
@@ -48,7 +46,7 @@ async def validate_credentials(
|
|||||||
return "unknown"
|
return "unknown"
|
||||||
_aw = AnglianWater(authenticator=auth)
|
_aw = AnglianWater(authenticator=auth)
|
||||||
try:
|
try:
|
||||||
await _aw.validate_smart_meter(account_number)
|
await _aw.validate_smart_meter()
|
||||||
except (InvalidAccountIdError, SmartMeterUnavailableError):
|
except (InvalidAccountIdError, SmartMeterUnavailableError):
|
||||||
return "smart_meter_unavailable"
|
return "smart_meter_unavailable"
|
||||||
return auth
|
return auth
|
||||||
@@ -71,12 +69,10 @@ class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self.hass,
|
self.hass,
|
||||||
cookie_jar=CookieJar(quote_cookie=False),
|
cookie_jar=CookieJar(quote_cookie=False),
|
||||||
),
|
),
|
||||||
),
|
account_number=user_input[CONF_ACCOUNT_NUMBER],
|
||||||
user_input[CONF_ACCOUNT_NUMBER],
|
|
||||||
)
|
)
|
||||||
if isinstance(validation_response, str):
|
)
|
||||||
errors["base"] = validation_response
|
if isinstance(validation_response, BaseAuth):
|
||||||
else:
|
|
||||||
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
|
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
@@ -86,6 +82,7 @@ class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_ACCESS_TOKEN: validation_response.refresh_token,
|
CONF_ACCESS_TOKEN: validation_response.refresh_token,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
errors["base"] = validation_response
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
type AnglianWaterConfigEntry = ConfigEntry[AnglianWaterUpdateCoordinator]
|
type AnglianWaterConfigEntry = ConfigEntry[AnglianWaterUpdateCoordinator]
|
||||||
|
|
||||||
@@ -44,6 +44,6 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||||||
async def _async_update_data(self) -> None:
|
async def _async_update_data(self) -> None:
|
||||||
"""Update data from Anglian Water's API."""
|
"""Update data from Anglian Water's API."""
|
||||||
try:
|
try:
|
||||||
return await self.api.update(self.config_entry.data[CONF_ACCOUNT_NUMBER])
|
return await self.api.update()
|
||||||
except (ExpiredAccessTokenError, UnknownEndpointError) as err:
|
except (ExpiredAccessTokenError, UnknownEndpointError) as err:
|
||||||
raise UpdateFailed from err
|
raise UpdateFailed from err
|
||||||
|
|||||||
@@ -4,9 +4,8 @@
|
|||||||
"codeowners": ["@pantherale0"],
|
"codeowners": ["@pantherale0"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
|
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyanglianwater"],
|
"loggers": ["pyanglianwater"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pyanglianwater==3.1.0"]
|
"requirements": ["pyanglianwater==2.1.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@Lash-L"],
|
"codeowners": ["@Lash-L"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/anova",
|
"documentation": "https://www.home-assistant.io/integrations/anova",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["anova_wifi"],
|
"loggers": ["anova_wifi"],
|
||||||
"requirements": ["anova-wifi==0.17.0"]
|
"requirements": ["anova-wifi==0.17.0"]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@hyralex"],
|
"codeowners": ["@hyralex"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/anthemav",
|
"documentation": "https://www.home-assistant.io/integrations/anthemav",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["anthemav"],
|
"loggers": ["anthemav"],
|
||||||
"requirements": ["anthemav==1.4.1"]
|
"requirements": ["anthemav==1.4.1"]
|
||||||
|
|||||||
@@ -421,8 +421,6 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
|||||||
)
|
)
|
||||||
if short_form.search(model_alias):
|
if short_form.search(model_alias):
|
||||||
model_alias += "-0"
|
model_alias += "-0"
|
||||||
if model_alias.endswith(("haiku", "opus", "sonnet")):
|
|
||||||
model_alias += "-latest"
|
|
||||||
model_options.append(
|
model_options.append(
|
||||||
SelectOptionDict(
|
SelectOptionDict(
|
||||||
label=model_info.display_name,
|
label=model_info.display_name,
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
identifiers={(DOMAIN, subentry.subentry_id)},
|
identifiers={(DOMAIN, subentry.subentry_id)},
|
||||||
name=subentry.title,
|
name=subentry.title,
|
||||||
manufacturer="Anthropic",
|
manufacturer="Anthropic",
|
||||||
model=subentry.data.get(CONF_CHAT_MODEL, DEFAULT[CONF_CHAT_MODEL]),
|
model="Claude",
|
||||||
entry_type=dr.DeviceEntryType.SERVICE,
|
entry_type=dr.DeviceEntryType.SERVICE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["anthropic==0.75.0"]
|
"requirements": ["anthropic==0.73.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@bdr99"],
|
"codeowners": ["@bdr99"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["py-aosmith==1.0.15"]
|
"requirements": ["py-aosmith==1.0.15"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@yuxincs"],
|
"codeowners": ["@yuxincs"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["apcaccess"],
|
"loggers": ["apcaccess"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@elupus"],
|
"codeowners": ["@elupus"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["arcam"],
|
"loggers": ["arcam"],
|
||||||
"requirements": ["arcam-fmj==1.8.2"],
|
"requirements": ["arcam-fmj==1.8.2"],
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@ikalnyi"],
|
"codeowners": ["@ikalnyi"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/arve",
|
"documentation": "https://www.home-assistant.io/integrations/arve",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["asyncarve==0.1.1"]
|
"requirements": ["asyncarve==0.1.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@milanmeu"],
|
"codeowners": ["@milanmeu"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
|
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioaseko"],
|
"loggers": ["aioaseko"],
|
||||||
"requirements": ["aioaseko==1.0.0"]
|
"requirements": ["aioaseko==1.0.0"]
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
import math
|
|
||||||
|
|
||||||
from pysilero_vad import SileroVoiceActivityDetector
|
from pymicro_vad import MicroVad
|
||||||
from pyspeex_noise import AudioProcessor
|
from pyspeex_noise import AudioProcessor
|
||||||
|
|
||||||
from .const import BYTES_PER_CHUNK
|
from .const import BYTES_PER_CHUNK
|
||||||
@@ -43,8 +42,8 @@ class AudioEnhancer(ABC):
|
|||||||
"""Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
"""Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||||
|
|
||||||
|
|
||||||
class SileroVadSpeexEnhancer(AudioEnhancer):
|
class MicroVadSpeexEnhancer(AudioEnhancer):
|
||||||
"""Audio enhancer that runs Silero VAD and speex."""
|
"""Audio enhancer that runs microVAD and speex."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool
|
self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool
|
||||||
@@ -70,49 +69,21 @@ class SileroVadSpeexEnhancer(AudioEnhancer):
|
|||||||
self.noise_suppression,
|
self.noise_suppression,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.vad: SileroVoiceActivityDetector | None = None
|
self.vad: MicroVad | None = None
|
||||||
|
|
||||||
# We get 10ms chunks but Silero works on 32ms chunks, so we have to
|
|
||||||
# buffer audio. The previous speech probability is used until enough
|
|
||||||
# audio has been buffered.
|
|
||||||
self._vad_buffer: bytearray | None = None
|
|
||||||
self._vad_buffer_chunks = 0
|
|
||||||
self._vad_buffer_chunk_idx = 0
|
|
||||||
self._last_speech_probability: float | None = None
|
|
||||||
|
|
||||||
if self.is_vad_enabled:
|
if self.is_vad_enabled:
|
||||||
self.vad = SileroVoiceActivityDetector()
|
self.vad = MicroVad()
|
||||||
|
_LOGGER.debug("Initialized microVAD")
|
||||||
# VAD buffer is a multiple of 10ms, but Silero VAD needs 32ms.
|
|
||||||
self._vad_buffer_chunks = int(
|
|
||||||
math.ceil(self.vad.chunk_bytes() / BYTES_PER_CHUNK)
|
|
||||||
)
|
|
||||||
self._vad_leftover_bytes = self.vad.chunk_bytes() - BYTES_PER_CHUNK
|
|
||||||
self._vad_buffer = bytearray(self.vad.chunk_bytes())
|
|
||||||
_LOGGER.debug("Initialized Silero VAD")
|
|
||||||
|
|
||||||
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
|
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
|
||||||
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
|
||||||
|
speech_probability: float | None = None
|
||||||
|
|
||||||
assert len(audio) == BYTES_PER_CHUNK
|
assert len(audio) == BYTES_PER_CHUNK
|
||||||
|
|
||||||
if self.vad is not None:
|
if self.vad is not None:
|
||||||
# Run VAD
|
# Run VAD
|
||||||
assert self._vad_buffer is not None
|
speech_probability = self.vad.Process10ms(audio)
|
||||||
start_idx = self._vad_buffer_chunk_idx * BYTES_PER_CHUNK
|
|
||||||
self._vad_buffer[start_idx : start_idx + BYTES_PER_CHUNK] = audio
|
|
||||||
|
|
||||||
self._vad_buffer_chunk_idx += 1
|
|
||||||
if self._vad_buffer_chunk_idx >= self._vad_buffer_chunks:
|
|
||||||
# We have enough data to run Silero VAD (32 ms)
|
|
||||||
self._last_speech_probability = self.vad.process_chunk(
|
|
||||||
self._vad_buffer[: self.vad.chunk_bytes()]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Copy leftover audio that wasn't processed to start
|
|
||||||
self._vad_buffer[: self._vad_leftover_bytes] = self._vad_buffer[
|
|
||||||
-self._vad_leftover_bytes :
|
|
||||||
]
|
|
||||||
self._vad_buffer_chunk_idx = 0
|
|
||||||
|
|
||||||
if self.audio_processor is not None:
|
if self.audio_processor is not None:
|
||||||
# Run noise suppression and auto gain
|
# Run noise suppression and auto gain
|
||||||
@@ -121,5 +92,5 @@ class SileroVadSpeexEnhancer(AudioEnhancer):
|
|||||||
return EnhancedAudioChunk(
|
return EnhancedAudioChunk(
|
||||||
audio=audio,
|
audio=audio,
|
||||||
timestamp_ms=timestamp_ms,
|
timestamp_ms=timestamp_ms,
|
||||||
speech_probability=self._last_speech_probability,
|
speech_probability=speech_probability,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["pysilero-vad==3.0.1", "pyspeex-noise==1.0.2"]
|
"requirements": ["pymicro-vad==1.0.1", "pyspeex-noise==1.0.2"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ from homeassistant.util import (
|
|||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
from homeassistant.util.limited_size_dict import LimitedSizeDict
|
from homeassistant.util.limited_size_dict import LimitedSizeDict
|
||||||
|
|
||||||
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, SileroVadSpeexEnhancer
|
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadSpeexEnhancer
|
||||||
from .const import (
|
from .const import (
|
||||||
ACKNOWLEDGE_PATH,
|
ACKNOWLEDGE_PATH,
|
||||||
BYTES_PER_CHUNK,
|
BYTES_PER_CHUNK,
|
||||||
@@ -633,7 +633,7 @@ class PipelineRun:
|
|||||||
# Initialize with audio settings
|
# Initialize with audio settings
|
||||||
if self.audio_settings.needs_processor and (self.audio_enhancer is None):
|
if self.audio_settings.needs_processor and (self.audio_enhancer is None):
|
||||||
# Default audio enhancer
|
# Default audio enhancer
|
||||||
self.audio_enhancer = SileroVadSpeexEnhancer(
|
self.audio_enhancer = MicroVadSpeexEnhancer(
|
||||||
self.audio_settings.auto_gain_dbfs,
|
self.audio_settings.auto_gain_dbfs,
|
||||||
self.audio_settings.noise_suppression_level,
|
self.audio_settings.noise_suppression_level,
|
||||||
self.audio_settings.is_vad_enabled,
|
self.audio_settings.is_vad_enabled,
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
"""Provides triggers for assist satellites."""
|
"""Provides triggers for assist satellites."""
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import AssistSatelliteState
|
from .entity import AssistSatelliteState
|
||||||
|
|
||||||
TRIGGERS: dict[str, type[Trigger]] = {
|
TRIGGERS: dict[str, type[Trigger]] = {
|
||||||
"idle": make_entity_target_state_trigger(DOMAIN, AssistSatelliteState.IDLE),
|
"idle": make_entity_state_trigger(DOMAIN, AssistSatelliteState.IDLE),
|
||||||
"listening": make_entity_target_state_trigger(
|
"listening": make_entity_state_trigger(DOMAIN, AssistSatelliteState.LISTENING),
|
||||||
DOMAIN, AssistSatelliteState.LISTENING
|
"processing": make_entity_state_trigger(DOMAIN, AssistSatelliteState.PROCESSING),
|
||||||
),
|
"responding": make_entity_state_trigger(DOMAIN, AssistSatelliteState.RESPONDING),
|
||||||
"processing": make_entity_target_state_trigger(
|
|
||||||
DOMAIN, AssistSatelliteState.PROCESSING
|
|
||||||
),
|
|
||||||
"responding": make_entity_target_state_trigger(
|
|
||||||
DOMAIN, AssistSatelliteState.RESPONDING
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
|
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
|
||||||
"requirements": ["aioasuswrt==1.5.4", "asusrouter==1.21.3"]
|
"requirements": ["aioasuswrt==1.5.2", "asusrouter==1.21.3"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@MatsNL"],
|
"codeowners": ["@MatsNL"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/atag",
|
"documentation": "https://www.home-assistant.io/integrations/atag",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyatag"],
|
"loggers": ["pyatag"],
|
||||||
"requirements": ["pyatag==0.3.5.3"]
|
"requirements": ["pyatag==0.3.5.3"]
|
||||||
|
|||||||
@@ -27,8 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pubnub", "yalexs"],
|
"loggers": ["pubnub", "yalexs"],
|
||||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.2"]
|
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@djtimca"],
|
"codeowners": ["@djtimca"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aurora",
|
"documentation": "https://www.home-assistant.io/integrations/aurora",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["auroranoaa"],
|
"loggers": ["auroranoaa"],
|
||||||
"requirements": ["auroranoaa==0.0.5"]
|
"requirements": ["auroranoaa==0.0.5"]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@nickw444", "@Bre77"],
|
"codeowners": ["@nickw444", "@Bre77"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aussie_broadband",
|
"documentation": "https://www.home-assistant.io/integrations/aussie_broadband",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aussiebb"],
|
"loggers": ["aussiebb"],
|
||||||
"requirements": ["pyaussiebb==0.1.5"]
|
"requirements": ["pyaussiebb==0.1.5"]
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
"codeowners": ["@klaasnicolaas"],
|
"codeowners": ["@klaasnicolaas"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/autarco",
|
"documentation": "https://www.home-assistant.io/integrations/autarco",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"quality_scale": "silver",
|
|
||||||
"requirements": ["autarco==3.2.0"]
|
"requirements": ["autarco==3.2.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ rules:
|
|||||||
This integration does not provide additional actions.
|
This integration does not provide additional actions.
|
||||||
appropriate-polling: done
|
appropriate-polling: done
|
||||||
brands: done
|
brands: done
|
||||||
common-modules: done
|
common-modules:
|
||||||
|
status: todo
|
||||||
|
comment: |
|
||||||
|
The entity.py file is not used in this integration.
|
||||||
config-flow-test-coverage: done
|
config-flow-test-coverage: done
|
||||||
config-flow: done
|
config-flow: done
|
||||||
dependency-transparency: done
|
dependency-transparency: done
|
||||||
|
|||||||
@@ -204,25 +204,13 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class AutarcoSensorBase(CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity):
|
class AutarcoBatterySensorEntity(
|
||||||
"""Base class for Autarco sensors."""
|
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
|
||||||
|
):
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: AutarcoDataUpdateCoordinator,
|
|
||||||
description: SensorEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize Autarco sensor base."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self.entity_description = description
|
|
||||||
|
|
||||||
|
|
||||||
class AutarcoBatterySensorEntity(AutarcoSensorBase):
|
|
||||||
"""Defines an Autarco battery sensor."""
|
"""Defines an Autarco battery sensor."""
|
||||||
|
|
||||||
entity_description: AutarcoBatterySensorEntityDescription
|
entity_description: AutarcoBatterySensorEntityDescription
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -230,8 +218,10 @@ class AutarcoBatterySensorEntity(AutarcoSensorBase):
|
|||||||
coordinator: AutarcoDataUpdateCoordinator,
|
coordinator: AutarcoDataUpdateCoordinator,
|
||||||
description: AutarcoBatterySensorEntityDescription,
|
description: AutarcoBatterySensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Autarco battery sensor."""
|
"""Initialize Autarco sensor."""
|
||||||
super().__init__(coordinator, description)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
self.entity_description = description
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{coordinator.account_site.site_id}_battery_{description.key}"
|
f"{coordinator.account_site.site_id}_battery_{description.key}"
|
||||||
)
|
)
|
||||||
@@ -249,10 +239,13 @@ class AutarcoBatterySensorEntity(AutarcoSensorBase):
|
|||||||
return self.entity_description.value_fn(self.coordinator.data.battery)
|
return self.entity_description.value_fn(self.coordinator.data.battery)
|
||||||
|
|
||||||
|
|
||||||
class AutarcoSolarSensorEntity(AutarcoSensorBase):
|
class AutarcoSolarSensorEntity(
|
||||||
|
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
|
||||||
|
):
|
||||||
"""Defines an Autarco solar sensor."""
|
"""Defines an Autarco solar sensor."""
|
||||||
|
|
||||||
entity_description: AutarcoSolarSensorEntityDescription
|
entity_description: AutarcoSolarSensorEntityDescription
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -260,8 +253,10 @@ class AutarcoSolarSensorEntity(AutarcoSensorBase):
|
|||||||
coordinator: AutarcoDataUpdateCoordinator,
|
coordinator: AutarcoDataUpdateCoordinator,
|
||||||
description: AutarcoSolarSensorEntityDescription,
|
description: AutarcoSolarSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Autarco solar sensor."""
|
"""Initialize Autarco sensor."""
|
||||||
super().__init__(coordinator, description)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
self.entity_description = description
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{coordinator.account_site.site_id}_solar_{description.key}"
|
f"{coordinator.account_site.site_id}_solar_{description.key}"
|
||||||
)
|
)
|
||||||
@@ -278,10 +273,13 @@ class AutarcoSolarSensorEntity(AutarcoSensorBase):
|
|||||||
return self.entity_description.value_fn(self.coordinator.data.solar)
|
return self.entity_description.value_fn(self.coordinator.data.solar)
|
||||||
|
|
||||||
|
|
||||||
class AutarcoInverterSensorEntity(AutarcoSensorBase):
|
class AutarcoInverterSensorEntity(
|
||||||
|
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
|
||||||
|
):
|
||||||
"""Defines an Autarco inverter sensor."""
|
"""Defines an Autarco inverter sensor."""
|
||||||
|
|
||||||
entity_description: AutarcoInverterSensorEntityDescription
|
entity_description: AutarcoInverterSensorEntityDescription
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -290,8 +288,10 @@ class AutarcoInverterSensorEntity(AutarcoSensorBase):
|
|||||||
description: AutarcoInverterSensorEntityDescription,
|
description: AutarcoInverterSensorEntityDescription,
|
||||||
serial_number: str,
|
serial_number: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Autarco inverter sensor."""
|
"""Initialize Autarco sensor."""
|
||||||
super().__init__(coordinator, description)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
self.entity_description = description
|
||||||
self._serial_number = serial_number
|
self._serial_number = serial_number
|
||||||
self._attr_unique_id = f"{serial_number}_{description.key}"
|
self._attr_unique_id = f"{serial_number}_{description.key}"
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
|
|||||||
@@ -125,18 +125,13 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
|||||||
"alarm_control_panel",
|
"alarm_control_panel",
|
||||||
"assist_satellite",
|
"assist_satellite",
|
||||||
"binary_sensor",
|
"binary_sensor",
|
||||||
"button",
|
|
||||||
"climate",
|
"climate",
|
||||||
"cover",
|
"cover",
|
||||||
"device_tracker",
|
|
||||||
"fan",
|
"fan",
|
||||||
"input_boolean",
|
|
||||||
"lawn_mower",
|
"lawn_mower",
|
||||||
"light",
|
"light",
|
||||||
"media_player",
|
"media_player",
|
||||||
"switch",
|
|
||||||
"text",
|
"text",
|
||||||
"update",
|
|
||||||
"vacuum",
|
"vacuum",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@kaareseras"],
|
"codeowners": ["@kaareseras"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/azure_data_explorer",
|
"documentation": "https://www.home-assistant.io/integrations/azure_data_explorer",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["azure"],
|
"loggers": ["azure"],
|
||||||
"requirements": ["azure-kusto-ingest==4.5.1", "azure-kusto-data[aio]==4.5.1"]
|
"requirements": ["azure-kusto-ingest==4.5.1", "azure-kusto-data[aio]==4.5.1"]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@timmo001"],
|
"codeowners": ["@timmo001"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/azure_devops",
|
"documentation": "https://www.home-assistant.io/integrations/azure_devops",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioazuredevops"],
|
"loggers": ["aioazuredevops"],
|
||||||
"requirements": ["aioazuredevops==2.2.2"]
|
"requirements": ["aioazuredevops==2.2.2"]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@eavanvalkenburg"],
|
"codeowners": ["@eavanvalkenburg"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/azure_event_hub",
|
"documentation": "https://www.home-assistant.io/integrations/azure_event_hub",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["azure"],
|
"loggers": ["azure"],
|
||||||
"requirements": ["azure-eventhub==5.11.1"],
|
"requirements": ["azure-eventhub==5.11.1"],
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@bdraco", "@jfroy"],
|
"codeowners": ["@bdraco", "@jfroy"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/baf",
|
"documentation": "https://www.home-assistant.io/integrations/baf",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["aiobafi6==0.9.0"],
|
"requirements": ["aiobafi6==0.9.0"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/balboa",
|
"documentation": "https://www.home-assistant.io/integrations/balboa",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pybalboa"],
|
"loggers": ["pybalboa"],
|
||||||
"requirements": ["pybalboa==1.1.3"]
|
"requirements": ["pybalboa==1.1.3"]
|
||||||
|
|||||||
@@ -21,29 +21,29 @@ from homeassistant.helpers import device_registry as dr
|
|||||||
from homeassistant.util.ssl import get_default_context
|
from homeassistant.util.ssl import get_default_context
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .websocket import BeoWebsocket
|
from .websocket import BangOlufsenWebsocket
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BeoData:
|
class BangOlufsenData:
|
||||||
"""Dataclass for API client and WebSocket client."""
|
"""Dataclass for API client and WebSocket client."""
|
||||||
|
|
||||||
websocket: BeoWebsocket
|
websocket: BangOlufsenWebsocket
|
||||||
client: MozartClient
|
client: MozartClient
|
||||||
|
|
||||||
|
|
||||||
type BeoConfigEntry = ConfigEntry[BeoData]
|
type BangOlufsenConfigEntry = ConfigEntry[BangOlufsenData]
|
||||||
|
|
||||||
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
|
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry) -> bool:
|
||||||
"""Set up from a config entry."""
|
"""Set up from a config entry."""
|
||||||
|
|
||||||
# Remove casts to str
|
# Remove casts to str
|
||||||
assert entry.unique_id
|
assert entry.unique_id
|
||||||
|
|
||||||
# Create device now as BeoWebsocket needs a device for debug logging, firing events etc.
|
# Create device now as BangOlufsenWebsocket needs a device for debug logging, firing events etc.
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
device_registry.async_get_or_create(
|
device_registry.async_get_or_create(
|
||||||
config_entry_id=entry.entry_id,
|
config_entry_id=entry.entry_id,
|
||||||
@@ -68,10 +68,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
|
|||||||
await client.close_api_client()
|
await client.close_api_client()
|
||||||
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error
|
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error
|
||||||
|
|
||||||
websocket = BeoWebsocket(hass, entry, client)
|
websocket = BangOlufsenWebsocket(hass, entry, client)
|
||||||
|
|
||||||
# Add the websocket and API client
|
# Add the websocket and API client
|
||||||
entry.runtime_data = BeoData(websocket, client)
|
entry.runtime_data = BangOlufsenData(websocket, client)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
@@ -82,7 +82,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: BangOlufsenConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
# Close the API client and WebSocket notification listener
|
# Close the API client and WebSocket notification listener
|
||||||
entry.runtime_data.client.disconnect_notifications()
|
entry.runtime_data.client.disconnect_notifications()
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ _exception_map = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BeoConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow."""
|
"""Handle a config flow."""
|
||||||
|
|
||||||
_beolink_jid = ""
|
_beolink_jid = ""
|
||||||
|
|||||||
@@ -14,26 +14,21 @@ from homeassistant.components.media_player import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BeoSource:
|
class BangOlufsenSource:
|
||||||
"""Class used for associating device source ids with friendly names. May not include all sources."""
|
"""Class used for associating device source ids with friendly names. May not include all sources."""
|
||||||
|
|
||||||
DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
|
|
||||||
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
|
LINE_IN: Final[Source] = Source(name="Line-In", id="lineIn")
|
||||||
NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
|
|
||||||
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
|
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
|
||||||
TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
|
|
||||||
TV: Final[Source] = Source(name="TV", id="tv")
|
|
||||||
UNKNOWN: Final[Source] = Source(name="Unknown Source", id="unknown")
|
|
||||||
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
|
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
|
||||||
|
|
||||||
|
|
||||||
BEO_STATES: dict[str, MediaPlayerState] = {
|
BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
|
||||||
# Dict used for translating device states to Home Assistant states.
|
# Dict used for translating device states to Home Assistant states.
|
||||||
"started": MediaPlayerState.PLAYING,
|
"started": MediaPlayerState.PLAYING,
|
||||||
"buffering": MediaPlayerState.PLAYING,
|
"buffering": MediaPlayerState.PLAYING,
|
||||||
"idle": MediaPlayerState.IDLE,
|
"idle": MediaPlayerState.IDLE,
|
||||||
"paused": MediaPlayerState.PAUSED,
|
"paused": MediaPlayerState.PAUSED,
|
||||||
"stopped": MediaPlayerState.IDLE,
|
"stopped": MediaPlayerState.PAUSED,
|
||||||
"ended": MediaPlayerState.PAUSED,
|
"ended": MediaPlayerState.PAUSED,
|
||||||
"error": MediaPlayerState.IDLE,
|
"error": MediaPlayerState.IDLE,
|
||||||
# A device's initial state is "unknown" and should be treated as "idle"
|
# A device's initial state is "unknown" and should be treated as "idle"
|
||||||
@@ -41,31 +36,30 @@ BEO_STATES: dict[str, MediaPlayerState] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Dict used for translating Home Assistant settings to device repeat settings.
|
# Dict used for translating Home Assistant settings to device repeat settings.
|
||||||
BEO_REPEAT_FROM_HA: dict[RepeatMode, str] = {
|
BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
|
||||||
RepeatMode.ALL: "all",
|
RepeatMode.ALL: "all",
|
||||||
RepeatMode.ONE: "track",
|
RepeatMode.ONE: "track",
|
||||||
RepeatMode.OFF: "none",
|
RepeatMode.OFF: "none",
|
||||||
}
|
}
|
||||||
# Dict used for translating device repeat settings to Home Assistant settings.
|
# Dict used for translating device repeat settings to Home Assistant settings.
|
||||||
BEO_REPEAT_TO_HA: dict[str, RepeatMode] = {
|
BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
|
||||||
value: key for key, value in BEO_REPEAT_FROM_HA.items()
|
value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Media types for play_media
|
# Media types for play_media
|
||||||
class BeoMediaType(StrEnum):
|
class BangOlufsenMediaType(StrEnum):
|
||||||
"""Bang & Olufsen specific media types."""
|
"""Bang & Olufsen specific media types."""
|
||||||
|
|
||||||
DEEZER = "deezer"
|
|
||||||
FAVOURITE = "favourite"
|
FAVOURITE = "favourite"
|
||||||
OVERLAY_TTS = "overlay_tts"
|
DEEZER = "deezer"
|
||||||
RADIO = "radio"
|
RADIO = "radio"
|
||||||
TIDAL = "tidal"
|
TIDAL = "tidal"
|
||||||
TTS = "provider"
|
TTS = "provider"
|
||||||
TV = "tv"
|
OVERLAY_TTS = "overlay_tts"
|
||||||
|
|
||||||
|
|
||||||
class BeoModel(StrEnum):
|
class BangOlufsenModel(StrEnum):
|
||||||
"""Enum for compatible model names."""
|
"""Enum for compatible model names."""
|
||||||
|
|
||||||
# Mozart devices
|
# Mozart devices
|
||||||
@@ -84,18 +78,8 @@ class BeoModel(StrEnum):
|
|||||||
BEOREMOTE_ONE = "Beoremote One"
|
BEOREMOTE_ONE = "Beoremote One"
|
||||||
|
|
||||||
|
|
||||||
class BeoAttribute(StrEnum):
|
|
||||||
"""Enum for extra_state_attribute keys."""
|
|
||||||
|
|
||||||
BEOLINK = "beolink"
|
|
||||||
BEOLINK_PEERS = "peers"
|
|
||||||
BEOLINK_SELF = "self"
|
|
||||||
BEOLINK_LEADER = "leader"
|
|
||||||
BEOLINK_LISTENERS = "listeners"
|
|
||||||
|
|
||||||
|
|
||||||
# Physical "buttons" on devices
|
# Physical "buttons" on devices
|
||||||
class BeoButtons(StrEnum):
|
class BangOlufsenButtons(StrEnum):
|
||||||
"""Enum for device buttons."""
|
"""Enum for device buttons."""
|
||||||
|
|
||||||
BLUETOOTH = "Bluetooth"
|
BLUETOOTH = "Bluetooth"
|
||||||
@@ -142,7 +126,7 @@ class WebsocketNotification(StrEnum):
|
|||||||
DOMAIN: Final[str] = "bang_olufsen"
|
DOMAIN: Final[str] = "bang_olufsen"
|
||||||
|
|
||||||
# Default values for configuration.
|
# Default values for configuration.
|
||||||
DEFAULT_MODEL: Final[str] = BeoModel.BEOSOUND_BALANCE
|
DEFAULT_MODEL: Final[str] = BangOlufsenModel.BEOSOUND_BALANCE
|
||||||
|
|
||||||
# Configuration.
|
# Configuration.
|
||||||
CONF_SERIAL_NUMBER: Final = "serial_number"
|
CONF_SERIAL_NUMBER: Final = "serial_number"
|
||||||
@@ -150,7 +134,7 @@ CONF_BEOLINK_JID: Final = "jid"
|
|||||||
|
|
||||||
# Models to choose from in manual configuration.
|
# Models to choose from in manual configuration.
|
||||||
SELECTABLE_MODELS: list[str] = [
|
SELECTABLE_MODELS: list[str] = [
|
||||||
model.value for model in BeoModel if model != BeoModel.BEOREMOTE_ONE
|
model.value for model in BangOlufsenModel if model != BangOlufsenModel.BEOREMOTE_ONE
|
||||||
]
|
]
|
||||||
|
|
||||||
MANUFACTURER: Final[str] = "Bang & Olufsen"
|
MANUFACTURER: Final[str] = "Bang & Olufsen"
|
||||||
@@ -162,15 +146,15 @@ ATTR_ITEM_NUMBER: Final[str] = "in"
|
|||||||
ATTR_FRIENDLY_NAME: Final[str] = "fn"
|
ATTR_FRIENDLY_NAME: Final[str] = "fn"
|
||||||
|
|
||||||
# Power states.
|
# Power states.
|
||||||
BEO_ON: Final[str] = "on"
|
BANG_OLUFSEN_ON: Final[str] = "on"
|
||||||
|
|
||||||
VALID_MEDIA_TYPES: Final[tuple] = (
|
VALID_MEDIA_TYPES: Final[tuple] = (
|
||||||
BeoMediaType.FAVOURITE,
|
BangOlufsenMediaType.FAVOURITE,
|
||||||
BeoMediaType.DEEZER,
|
BangOlufsenMediaType.DEEZER,
|
||||||
BeoMediaType.RADIO,
|
BangOlufsenMediaType.RADIO,
|
||||||
BeoMediaType.TTS,
|
BangOlufsenMediaType.TTS,
|
||||||
BeoMediaType.TIDAL,
|
BangOlufsenMediaType.TIDAL,
|
||||||
BeoMediaType.OVERLAY_TTS,
|
BangOlufsenMediaType.OVERLAY_TTS,
|
||||||
MediaType.MUSIC,
|
MediaType.MUSIC,
|
||||||
MediaType.URL,
|
MediaType.URL,
|
||||||
MediaType.CHANNEL,
|
MediaType.CHANNEL,
|
||||||
@@ -248,7 +232,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Device events
|
# Device events
|
||||||
BEO_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
|
BANG_OLUFSEN_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
|
||||||
|
|
||||||
# Dict used to translate native Bang & Olufsen event names to string.json compatible ones
|
# Dict used to translate native Bang & Olufsen event names to string.json compatible ones
|
||||||
EVENT_TRANSLATION_MAP: dict[str, str] = {
|
EVENT_TRANSLATION_MAP: dict[str, str] = {
|
||||||
@@ -265,7 +249,7 @@ EVENT_TRANSLATION_MAP: dict[str, str] = {
|
|||||||
|
|
||||||
CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS"
|
CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS"
|
||||||
|
|
||||||
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BeoButtons]
|
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BangOlufsenButtons]
|
||||||
|
|
||||||
|
|
||||||
DEVICE_BUTTON_EVENTS: Final[list[str]] = [
|
DEVICE_BUTTON_EVENTS: Final[list[str]] = [
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ from homeassistant.const import CONF_MODEL
|
|||||||
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
|
||||||
|
|
||||||
from . import BeoConfigEntry
|
from . import BangOlufsenConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .util import get_device_buttons
|
from .util import get_device_buttons
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, config_entry: BeoConfigEntry
|
hass: HomeAssistant, config_entry: BangOlufsenConfigEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ from homeassistant.helpers.entity import Entity
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
class BeoBase:
|
class BangOlufsenBase:
|
||||||
"""Base class for Bang & Olufsen Home Assistant objects."""
|
"""Base class for BangOlufsen Home Assistant objects."""
|
||||||
|
|
||||||
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
|
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
|
||||||
"""Initialize the object."""
|
"""Initialize the object."""
|
||||||
@@ -51,8 +51,8 @@ class BeoBase:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BeoEntity(Entity, BeoBase):
|
class BangOlufsenEntity(Entity, BangOlufsenBase):
|
||||||
"""Base Entity for Bang & Olufsen entities."""
|
"""Base Entity for BangOlufsen entities."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import BeoConfigEntry
|
from . import BangOlufsenConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
BEO_REMOTE_CONTROL_KEYS,
|
BEO_REMOTE_CONTROL_KEYS,
|
||||||
BEO_REMOTE_KEY_EVENTS,
|
BEO_REMOTE_KEY_EVENTS,
|
||||||
@@ -25,10 +25,10 @@ from .const import (
|
|||||||
DEVICE_BUTTON_EVENTS,
|
DEVICE_BUTTON_EVENTS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
MANUFACTURER,
|
MANUFACTURER,
|
||||||
BeoModel,
|
BangOlufsenModel,
|
||||||
WebsocketNotification,
|
WebsocketNotification,
|
||||||
)
|
)
|
||||||
from .entity import BeoEntity
|
from .entity import BangOlufsenEntity
|
||||||
from .util import get_device_buttons, get_remotes
|
from .util import get_device_buttons, get_remotes
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@@ -36,14 +36,14 @@ PARALLEL_UPDATES = 0
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: BeoConfigEntry,
|
config_entry: BangOlufsenConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Event entities from config entry."""
|
"""Set up Event entities from config entry."""
|
||||||
entities: list[BeoEvent] = []
|
entities: list[BangOlufsenEvent] = []
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
BeoButtonEvent(config_entry, button_type)
|
BangOlufsenButtonEvent(config_entry, button_type)
|
||||||
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
|
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ async def async_setup_entry(
|
|||||||
# Add Light keys
|
# Add Light keys
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
[
|
||||||
BeoRemoteKeyEvent(
|
BangOlufsenRemoteKeyEvent(
|
||||||
config_entry,
|
config_entry,
|
||||||
remote,
|
remote,
|
||||||
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
|
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
|
||||||
@@ -66,7 +66,7 @@ async def async_setup_entry(
|
|||||||
# Add Control keys
|
# Add Control keys
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
[
|
||||||
BeoRemoteKeyEvent(
|
BangOlufsenRemoteKeyEvent(
|
||||||
config_entry,
|
config_entry,
|
||||||
remote,
|
remote,
|
||||||
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
|
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
|
||||||
@@ -84,9 +84,10 @@ async def async_setup_entry(
|
|||||||
config_entry.entry_id
|
config_entry.entry_id
|
||||||
)
|
)
|
||||||
for device in devices:
|
for device in devices:
|
||||||
if device.model == BeoModel.BEOREMOTE_ONE and device.serial_number not in {
|
if (
|
||||||
remote.serial_number for remote in remotes
|
device.model == BangOlufsenModel.BEOREMOTE_ONE
|
||||||
}:
|
and device.serial_number not in {remote.serial_number for remote in remotes}
|
||||||
|
):
|
||||||
device_registry.async_update_device(
|
device_registry.async_update_device(
|
||||||
device.id, remove_config_entry_id=config_entry.entry_id
|
device.id, remove_config_entry_id=config_entry.entry_id
|
||||||
)
|
)
|
||||||
@@ -94,13 +95,13 @@ async def async_setup_entry(
|
|||||||
async_add_entities(new_entities=entities)
|
async_add_entities(new_entities=entities)
|
||||||
|
|
||||||
|
|
||||||
class BeoEvent(BeoEntity, EventEntity):
|
class BangOlufsenEvent(BangOlufsenEntity, EventEntity):
|
||||||
"""Base Event class."""
|
"""Base Event class."""
|
||||||
|
|
||||||
_attr_device_class = EventDeviceClass.BUTTON
|
_attr_device_class = EventDeviceClass.BUTTON
|
||||||
_attr_entity_registry_enabled_default = False
|
_attr_entity_registry_enabled_default = False
|
||||||
|
|
||||||
def __init__(self, config_entry: BeoConfigEntry) -> None:
|
def __init__(self, config_entry: BangOlufsenConfigEntry) -> None:
|
||||||
"""Initialize Event."""
|
"""Initialize Event."""
|
||||||
super().__init__(config_entry, config_entry.runtime_data.client)
|
super().__init__(config_entry, config_entry.runtime_data.client)
|
||||||
|
|
||||||
@@ -111,12 +112,12 @@ class BeoEvent(BeoEntity, EventEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class BeoButtonEvent(BeoEvent):
|
class BangOlufsenButtonEvent(BangOlufsenEvent):
|
||||||
"""Event class for Button events."""
|
"""Event class for Button events."""
|
||||||
|
|
||||||
_attr_event_types = DEVICE_BUTTON_EVENTS
|
_attr_event_types = DEVICE_BUTTON_EVENTS
|
||||||
|
|
||||||
def __init__(self, config_entry: BeoConfigEntry, button_type: str) -> None:
|
def __init__(self, config_entry: BangOlufsenConfigEntry, button_type: str) -> None:
|
||||||
"""Initialize Button."""
|
"""Initialize Button."""
|
||||||
super().__init__(config_entry)
|
super().__init__(config_entry)
|
||||||
|
|
||||||
@@ -145,14 +146,14 @@ class BeoButtonEvent(BeoEvent):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BeoRemoteKeyEvent(BeoEvent):
|
class BangOlufsenRemoteKeyEvent(BangOlufsenEvent):
|
||||||
"""Event class for Beoremote One key events."""
|
"""Event class for Beoremote One key events."""
|
||||||
|
|
||||||
_attr_event_types = BEO_REMOTE_KEY_EVENTS
|
_attr_event_types = BEO_REMOTE_KEY_EVENTS
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config_entry: BeoConfigEntry,
|
config_entry: BangOlufsenConfigEntry,
|
||||||
remote: PairedRemote,
|
remote: PairedRemote,
|
||||||
key_type: str,
|
key_type: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -165,8 +166,8 @@ class BeoRemoteKeyEvent(BeoEvent):
|
|||||||
self._attr_unique_id = f"{remote.serial_number}_{self._unique_id}_{key_type}"
|
self._attr_unique_id = f"{remote.serial_number}_{self._unique_id}_{key_type}"
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")},
|
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")},
|
||||||
name=f"{BeoModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
|
name=f"{BangOlufsenModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
|
||||||
model=BeoModel.BEOREMOTE_ONE,
|
model=BangOlufsenModel.BEOREMOTE_ONE,
|
||||||
serial_number=remote.serial_number,
|
serial_number=remote.serial_number,
|
||||||
sw_version=remote.app_version,
|
sw_version=remote.app_version,
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
|
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["mozart-api==5.3.1.108.0"],
|
"requirements": ["mozart-api==5.1.0.247.1"],
|
||||||
"zeroconf": ["_bangolufsen._tcp.local."]
|
"zeroconf": ["_bangolufsen._tcp.local."]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,11 +69,11 @@ from homeassistant.helpers.entity_platform import (
|
|||||||
)
|
)
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from . import BeoConfigEntry
|
from . import BangOlufsenConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
BEO_REPEAT_FROM_HA,
|
BANG_OLUFSEN_REPEAT_FROM_HA,
|
||||||
BEO_REPEAT_TO_HA,
|
BANG_OLUFSEN_REPEAT_TO_HA,
|
||||||
BEO_STATES,
|
BANG_OLUFSEN_STATES,
|
||||||
BEOLINK_JOIN_SOURCES,
|
BEOLINK_JOIN_SOURCES,
|
||||||
BEOLINK_JOIN_SOURCES_TO_UPPER,
|
BEOLINK_JOIN_SOURCES_TO_UPPER,
|
||||||
CONF_BEOLINK_JID,
|
CONF_BEOLINK_JID,
|
||||||
@@ -82,12 +82,11 @@ from .const import (
|
|||||||
FALLBACK_SOURCES,
|
FALLBACK_SOURCES,
|
||||||
MANUFACTURER,
|
MANUFACTURER,
|
||||||
VALID_MEDIA_TYPES,
|
VALID_MEDIA_TYPES,
|
||||||
BeoAttribute,
|
BangOlufsenMediaType,
|
||||||
BeoMediaType,
|
BangOlufsenSource,
|
||||||
BeoSource,
|
|
||||||
WebsocketNotification,
|
WebsocketNotification,
|
||||||
)
|
)
|
||||||
from .entity import BeoEntity
|
from .entity import BangOlufsenEntity
|
||||||
from .util import get_serial_number_from_jid
|
from .util import get_serial_number_from_jid
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@@ -96,7 +95,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
BEO_FEATURES = (
|
BANG_OLUFSEN_FEATURES = (
|
||||||
MediaPlayerEntityFeature.BROWSE_MEDIA
|
MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||||
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
|
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
|
||||||
| MediaPlayerEntityFeature.GROUPING
|
| MediaPlayerEntityFeature.GROUPING
|
||||||
@@ -119,13 +118,15 @@ BEO_FEATURES = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: BeoConfigEntry,
|
config_entry: BangOlufsenConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up a Media Player entity from config entry."""
|
"""Set up a Media Player entity from config entry."""
|
||||||
# Add MediaPlayer entity
|
# Add MediaPlayer entity
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
new_entities=[BeoMediaPlayer(config_entry, config_entry.runtime_data.client)],
|
new_entities=[
|
||||||
|
BangOlufsenMediaPlayer(config_entry, config_entry.runtime_data.client)
|
||||||
|
],
|
||||||
update_before_add=True,
|
update_before_add=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -185,7 +186,7 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||||
"""Representation of a media player."""
|
"""Representation of a media player."""
|
||||||
|
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
@@ -218,14 +219,12 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
self._sources: dict[str, str] = {}
|
self._sources: dict[str, str] = {}
|
||||||
self._state: str = MediaPlayerState.IDLE
|
self._state: str = MediaPlayerState.IDLE
|
||||||
self._video_sources: dict[str, str] = {}
|
self._video_sources: dict[str, str] = {}
|
||||||
self._video_source_id_map: dict[str, str] = {}
|
|
||||||
self._sound_modes: dict[str, int] = {}
|
self._sound_modes: dict[str, int] = {}
|
||||||
|
|
||||||
# Beolink compatible sources
|
# Beolink compatible sources
|
||||||
self._beolink_sources: dict[str, bool] = {}
|
self._beolink_sources: dict[str, bool] = {}
|
||||||
self._remote_leader: BeolinkLeader | None = None
|
self._remote_leader: BeolinkLeader | None = None
|
||||||
# Extra state attributes:
|
# Extra state attributes for showing Beolink: peer(s), listener(s), leader and self
|
||||||
# Beolink: peer(s), listener(s), leader and self
|
|
||||||
self._beolink_attributes: dict[str, dict[str, dict[str, str]]] = {}
|
self._beolink_attributes: dict[str, dict[str, dict[str, str]]] = {}
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
@@ -287,7 +286,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
queue_settings = await self._client.get_settings_queue(_request_timeout=5)
|
queue_settings = await self._client.get_settings_queue(_request_timeout=5)
|
||||||
|
|
||||||
if queue_settings.repeat is not None:
|
if queue_settings.repeat is not None:
|
||||||
self._attr_repeat = BEO_REPEAT_TO_HA[queue_settings.repeat]
|
self._attr_repeat = BANG_OLUFSEN_REPEAT_TO_HA[queue_settings.repeat]
|
||||||
|
|
||||||
if queue_settings.shuffle is not None:
|
if queue_settings.shuffle is not None:
|
||||||
self._attr_shuffle = queue_settings.shuffle
|
self._attr_shuffle = queue_settings.shuffle
|
||||||
@@ -356,9 +355,6 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
and menu_item.label != "TV"
|
and menu_item.label != "TV"
|
||||||
):
|
):
|
||||||
self._video_sources[key] = menu_item.label
|
self._video_sources[key] = menu_item.label
|
||||||
self._video_source_id_map[
|
|
||||||
menu_item.content.content_uri.removeprefix("tv://")
|
|
||||||
] = menu_item.label
|
|
||||||
|
|
||||||
# Combine the source dicts
|
# Combine the source dicts
|
||||||
self._sources = self._audio_sources | self._video_sources
|
self._sources = self._audio_sources | self._video_sources
|
||||||
@@ -410,8 +406,8 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
|
|
||||||
# Check if source is line-in or optical and progress should be updated
|
# Check if source is line-in or optical and progress should be updated
|
||||||
if self._source_change.id in (
|
if self._source_change.id in (
|
||||||
BeoSource.LINE_IN.id,
|
BangOlufsenSource.LINE_IN.id,
|
||||||
BeoSource.SPDIF.id,
|
BangOlufsenSource.SPDIF.id,
|
||||||
):
|
):
|
||||||
self._playback_progress = PlaybackProgress(progress=0)
|
self._playback_progress = PlaybackProgress(progress=0)
|
||||||
|
|
||||||
@@ -440,10 +436,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
await self._async_update_beolink()
|
await self._async_update_beolink()
|
||||||
|
|
||||||
async def _async_update_beolink(self) -> None:
|
async def _async_update_beolink(self) -> None:
|
||||||
"""Update the current Beolink leader, listeners, peers and self.
|
"""Update the current Beolink leader, listeners, peers and self."""
|
||||||
|
|
||||||
Updates Home Assistant state.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._beolink_attributes = {}
|
self._beolink_attributes = {}
|
||||||
|
|
||||||
@@ -452,22 +445,18 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
|
|
||||||
# Add Beolink self
|
# Add Beolink self
|
||||||
self._beolink_attributes = {
|
self._beolink_attributes = {
|
||||||
BeoAttribute.BEOLINK: {
|
"beolink": {"self": {self.device_entry.name: self._beolink_jid}}
|
||||||
BeoAttribute.BEOLINK_SELF: {self.device_entry.name: self._beolink_jid}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add Beolink peers
|
# Add Beolink peers
|
||||||
peers = await self._client.get_beolink_peers()
|
peers = await self._client.get_beolink_peers()
|
||||||
|
|
||||||
if len(peers) > 0:
|
if len(peers) > 0:
|
||||||
self._beolink_attributes[BeoAttribute.BEOLINK][
|
self._beolink_attributes["beolink"]["peers"] = {}
|
||||||
BeoAttribute.BEOLINK_PEERS
|
|
||||||
] = {}
|
|
||||||
for peer in peers:
|
for peer in peers:
|
||||||
self._beolink_attributes[BeoAttribute.BEOLINK][
|
self._beolink_attributes["beolink"]["peers"][peer.friendly_name] = (
|
||||||
BeoAttribute.BEOLINK_PEERS
|
peer.jid
|
||||||
][peer.friendly_name] = peer.jid
|
)
|
||||||
|
|
||||||
# Add Beolink listeners / leader
|
# Add Beolink listeners / leader
|
||||||
self._remote_leader = self._playback_metadata.remote_leader
|
self._remote_leader = self._playback_metadata.remote_leader
|
||||||
@@ -488,9 +477,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
# Add self
|
# Add self
|
||||||
group_members.append(self.entity_id)
|
group_members.append(self.entity_id)
|
||||||
|
|
||||||
self._beolink_attributes[BeoAttribute.BEOLINK][
|
self._beolink_attributes["beolink"]["leader"] = {
|
||||||
BeoAttribute.BEOLINK_LEADER
|
|
||||||
] = {
|
|
||||||
self._remote_leader.friendly_name: self._remote_leader.jid,
|
self._remote_leader.friendly_name: self._remote_leader.jid,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,9 +514,9 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
beolink_listener.jid
|
beolink_listener.jid
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
self._beolink_attributes[BeoAttribute.BEOLINK][
|
self._beolink_attributes["beolink"]["listeners"] = (
|
||||||
BeoAttribute.BEOLINK_LISTENERS
|
beolink_listeners_attribute
|
||||||
] = beolink_listeners_attribute
|
)
|
||||||
|
|
||||||
self._attr_group_members = group_members
|
self._attr_group_members = group_members
|
||||||
|
|
||||||
@@ -587,7 +574,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
for sound_mode in sound_modes:
|
for sound_mode in sound_modes:
|
||||||
label = f"{sound_mode.name} ({sound_mode.id})"
|
label = f"{sound_mode.name} ({sound_mode.id})"
|
||||||
|
|
||||||
self._sound_modes[label] = cast(int, sound_mode.id)
|
self._sound_modes[label] = sound_mode.id
|
||||||
|
|
||||||
if sound_mode.id == active_sound_mode.id:
|
if sound_mode.id == active_sound_mode.id:
|
||||||
self._attr_sound_mode = label
|
self._attr_sound_mode = label
|
||||||
@@ -600,7 +587,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
@property
|
@property
|
||||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
def supported_features(self) -> MediaPlayerEntityFeature:
|
||||||
"""Flag media player features that are supported."""
|
"""Flag media player features that are supported."""
|
||||||
features = BEO_FEATURES
|
features = BANG_OLUFSEN_FEATURES
|
||||||
|
|
||||||
# Add seeking if supported by the current source
|
# Add seeking if supported by the current source
|
||||||
if self._source_change.is_seekable is True:
|
if self._source_change.is_seekable is True:
|
||||||
@@ -611,7 +598,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
@property
|
@property
|
||||||
def state(self) -> MediaPlayerState:
|
def state(self) -> MediaPlayerState:
|
||||||
"""Return the current state of the media player."""
|
"""Return the current state of the media player."""
|
||||||
return BEO_STATES[self._state]
|
return BANG_OLUFSEN_STATES[self._state]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume_level(self) -> float | None:
|
def volume_level(self) -> float | None:
|
||||||
@@ -628,19 +615,11 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_content_type(self) -> MediaType | str | None:
|
def media_content_type(self) -> str:
|
||||||
"""Return the current media type."""
|
"""Return the current media type."""
|
||||||
content_type = {
|
# Hard to determine content type
|
||||||
BeoSource.DEEZER.id: BeoMediaType.DEEZER,
|
if self._source_change.id == BangOlufsenSource.URI_STREAMER.id:
|
||||||
BeoSource.NET_RADIO.id: BeoMediaType.RADIO,
|
return MediaType.URL
|
||||||
BeoSource.TIDAL.id: BeoMediaType.TIDAL,
|
|
||||||
BeoSource.TV.id: BeoMediaType.TV,
|
|
||||||
BeoSource.URI_STREAMER.id: MediaType.URL,
|
|
||||||
}
|
|
||||||
# Hard to determine content type.
|
|
||||||
if self._source_change.id in content_type:
|
|
||||||
return content_type[self._source_change.id]
|
|
||||||
|
|
||||||
return MediaType.MUSIC
|
return MediaType.MUSIC
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -653,11 +632,6 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
"""Return the current playback progress."""
|
"""Return the current playback progress."""
|
||||||
return self._playback_progress.progress
|
return self._playback_progress.progress
|
||||||
|
|
||||||
@property
|
|
||||||
def media_content_id(self) -> str | None:
|
|
||||||
"""Return internal ID of Deezer, Tidal and radio stations."""
|
|
||||||
return self._playback_metadata.source_internal_id
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_image_url(self) -> str | None:
|
def media_image_url(self) -> str | None:
|
||||||
"""Return URL of the currently playing music."""
|
"""Return URL of the currently playing music."""
|
||||||
@@ -695,11 +669,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def source(self) -> str | None:
|
def source(self) -> str | None:
|
||||||
"""Return the current audio/video source."""
|
"""Return the current audio source."""
|
||||||
# Associate TV content ID with a video source
|
|
||||||
if self.media_content_id in self._video_source_id_map:
|
|
||||||
return self._video_source_id_map[self.media_content_id]
|
|
||||||
|
|
||||||
return self._source_change.name
|
return self._source_change.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -770,7 +740,9 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
async def async_set_repeat(self, repeat: RepeatMode) -> None:
|
async def async_set_repeat(self, repeat: RepeatMode) -> None:
|
||||||
"""Set playback queues to repeat."""
|
"""Set playback queues to repeat."""
|
||||||
await self._client.set_settings_queue(
|
await self._client.set_settings_queue(
|
||||||
play_queue_settings=PlayQueueSettings(repeat=BEO_REPEAT_FROM_HA[repeat])
|
play_queue_settings=PlayQueueSettings(
|
||||||
|
repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_set_shuffle(self, shuffle: bool) -> None:
|
async def async_set_shuffle(self, shuffle: bool) -> None:
|
||||||
@@ -874,7 +846,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
self._volume.level.level + offset_volume, 100
|
self._volume.level.level + offset_volume, 100
|
||||||
)
|
)
|
||||||
|
|
||||||
if media_type == BeoMediaType.OVERLAY_TTS:
|
if media_type == BangOlufsenMediaType.OVERLAY_TTS:
|
||||||
# Bang & Olufsen cloud TTS
|
# Bang & Olufsen cloud TTS
|
||||||
overlay_play_request.text_to_speech = (
|
overlay_play_request.text_to_speech = (
|
||||||
OverlayPlayRequestTextToSpeechTextToSpeech(
|
OverlayPlayRequestTextToSpeechTextToSpeech(
|
||||||
@@ -891,14 +863,14 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
|
|
||||||
# The "provider" media_type may not be suitable for overlay all the time.
|
# The "provider" media_type may not be suitable for overlay all the time.
|
||||||
# Use it for now.
|
# Use it for now.
|
||||||
elif media_type == BeoMediaType.TTS:
|
elif media_type == BangOlufsenMediaType.TTS:
|
||||||
await self._client.post_overlay_play(
|
await self._client.post_overlay_play(
|
||||||
overlay_play_request=OverlayPlayRequest(
|
overlay_play_request=OverlayPlayRequest(
|
||||||
uri=Uri(location=media_id),
|
uri=Uri(location=media_id),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
elif media_type == BeoMediaType.RADIO:
|
elif media_type == BangOlufsenMediaType.RADIO:
|
||||||
await self._client.run_provided_scene(
|
await self._client.run_provided_scene(
|
||||||
scene_properties=SceneProperties(
|
scene_properties=SceneProperties(
|
||||||
action_list=[
|
action_list=[
|
||||||
@@ -910,13 +882,13 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
elif media_type == BeoMediaType.FAVOURITE:
|
elif media_type == BangOlufsenMediaType.FAVOURITE:
|
||||||
await self._client.activate_preset(id=int(media_id))
|
await self._client.activate_preset(id=int(media_id))
|
||||||
|
|
||||||
elif media_type in (BeoMediaType.DEEZER, BeoMediaType.TIDAL):
|
elif media_type in (BangOlufsenMediaType.DEEZER, BangOlufsenMediaType.TIDAL):
|
||||||
try:
|
try:
|
||||||
# Play Deezer flow.
|
# Play Deezer flow.
|
||||||
if media_id == "flow" and media_type == BeoMediaType.DEEZER:
|
if media_id == "flow" and media_type == BangOlufsenMediaType.DEEZER:
|
||||||
deezer_id = None
|
deezer_id = None
|
||||||
|
|
||||||
if "id" in kwargs[ATTR_MEDIA_EXTRA]:
|
if "id" in kwargs[ATTR_MEDIA_EXTRA]:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.device_registry import DeviceEntry
|
from homeassistant.helpers.device_registry import DeviceEntry
|
||||||
|
|
||||||
from .const import DEVICE_BUTTONS, DOMAIN, BeoButtons, BeoModel
|
from .const import DEVICE_BUTTONS, DOMAIN, BangOlufsenButtons, BangOlufsenModel
|
||||||
|
|
||||||
|
|
||||||
def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
|
def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
|
||||||
@@ -40,27 +40,16 @@ async def get_remotes(client: MozartClient) -> list[PairedRemote]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_device_buttons(model: BeoModel) -> list[str]:
|
def get_device_buttons(model: BangOlufsenModel) -> list[str]:
|
||||||
"""Get supported buttons for a given model."""
|
"""Get supported buttons for a given model."""
|
||||||
# Beoconnect Core does not have any buttons
|
|
||||||
if model == BeoModel.BEOCONNECT_CORE:
|
|
||||||
return []
|
|
||||||
|
|
||||||
buttons = DEVICE_BUTTONS.copy()
|
buttons = DEVICE_BUTTONS.copy()
|
||||||
|
|
||||||
# Models that don't have a microphone button
|
# Beosound Premiere does not have a bluetooth button
|
||||||
if model in (
|
if model == BangOlufsenModel.BEOSOUND_PREMIERE:
|
||||||
BeoModel.BEOSOUND_A5,
|
buttons.remove(BangOlufsenButtons.BLUETOOTH)
|
||||||
BeoModel.BEOSOUND_A9,
|
|
||||||
BeoModel.BEOSOUND_PREMIERE,
|
|
||||||
):
|
|
||||||
buttons.remove(BeoButtons.MICROPHONE)
|
|
||||||
|
|
||||||
# Models that don't have a Bluetooth button
|
# Beoconnect Core does not have any buttons
|
||||||
if model in (
|
elif model == BangOlufsenModel.BEOCONNECT_CORE:
|
||||||
BeoModel.BEOSOUND_A9,
|
buttons = []
|
||||||
BeoModel.BEOSOUND_PREMIERE,
|
|
||||||
):
|
|
||||||
buttons.remove(BeoButtons.BLUETOOTH)
|
|
||||||
|
|
||||||
return buttons
|
return buttons
|
||||||
|
|||||||
@@ -27,20 +27,20 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|||||||
from homeassistant.util.enum import try_parse_enum
|
from homeassistant.util.enum import try_parse_enum
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
BEO_WEBSOCKET_EVENT,
|
BANG_OLUFSEN_WEBSOCKET_EVENT,
|
||||||
CONNECTION_STATUS,
|
CONNECTION_STATUS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_TRANSLATION_MAP,
|
EVENT_TRANSLATION_MAP,
|
||||||
BeoModel,
|
BangOlufsenModel,
|
||||||
WebsocketNotification,
|
WebsocketNotification,
|
||||||
)
|
)
|
||||||
from .entity import BeoBase
|
from .entity import BangOlufsenBase
|
||||||
from .util import get_device, get_remotes
|
from .util import get_device, get_remotes
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BeoWebsocket(BeoBase):
|
class BangOlufsenWebsocket(BangOlufsenBase):
|
||||||
"""The WebSocket listeners."""
|
"""The WebSocket listeners."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -48,7 +48,7 @@ class BeoWebsocket(BeoBase):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the WebSocket listeners."""
|
"""Initialize the WebSocket listeners."""
|
||||||
|
|
||||||
BeoBase.__init__(self, entry, client)
|
BangOlufsenBase.__init__(self, entry, client)
|
||||||
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._device = get_device(hass, self._unique_id)
|
self._device = get_device(hass, self._unique_id)
|
||||||
@@ -178,7 +178,7 @@ class BeoWebsocket(BeoBase):
|
|||||||
self.entry.entry_id
|
self.entry.entry_id
|
||||||
)
|
)
|
||||||
if device.serial_number is not None
|
if device.serial_number is not None
|
||||||
and device.model == BeoModel.BEOREMOTE_ONE
|
and device.model == BangOlufsenModel.BEOREMOTE_ONE
|
||||||
]
|
]
|
||||||
# Get paired remotes from device
|
# Get paired remotes from device
|
||||||
remote_serial_numbers = [
|
remote_serial_numbers = [
|
||||||
@@ -274,4 +274,4 @@ class BeoWebsocket(BeoBase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
_LOGGER.debug("%s", debug_notification)
|
_LOGGER.debug("%s", debug_notification)
|
||||||
self.hass.bus.async_fire(BEO_WEBSOCKET_EVENT, debug_notification)
|
self.hass.bus.async_fire(BANG_OLUFSEN_WEBSOCKET_EVENT, debug_notification)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from homeassistant.const import STATE_OFF, STATE_ON
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity import get_device_class
|
from homeassistant.helpers.entity import get_device_class
|
||||||
from homeassistant.helpers.trigger import EntityTargetStateTriggerBase, Trigger
|
from homeassistant.helpers.trigger import EntityStateTriggerBase, Trigger
|
||||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||||
|
|
||||||
from . import DOMAIN, BinarySensorDeviceClass
|
from . import DOMAIN, BinarySensorDeviceClass
|
||||||
@@ -20,7 +20,7 @@ def get_device_class_or_undefined(
|
|||||||
return UNDEFINED
|
return UNDEFINED
|
||||||
|
|
||||||
|
|
||||||
class BinarySensorOnOffTrigger(EntityTargetStateTriggerBase):
|
class BinarySensorOnOffTrigger(EntityStateTriggerBase):
|
||||||
"""Class for binary sensor on/off triggers."""
|
"""Class for binary sensor on/off triggers."""
|
||||||
|
|
||||||
_device_class: BinarySensorDeviceClass | None
|
_device_class: BinarySensorDeviceClass | None
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@bbx-a", "@swistakm"],
|
"codeowners": ["@bbx-a", "@swistakm"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/blebox",
|
"documentation": "https://www.home-assistant.io/integrations/blebox",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["blebox_uniapi"],
|
"loggers": ["blebox_uniapi"],
|
||||||
"requirements": ["blebox-uniapi==2.5.0"],
|
"requirements": ["blebox-uniapi==2.5.0"],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "blink",
|
"domain": "blink",
|
||||||
"name": "Blink",
|
"name": "Blink",
|
||||||
"codeowners": ["@fronzbot"],
|
"codeowners": ["@fronzbot", "@mkmer"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dhcp": [
|
"dhcp": [
|
||||||
{
|
{
|
||||||
@@ -18,8 +18,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/blink",
|
"documentation": "https://www.home-assistant.io/integrations/blink",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["blinkpy"],
|
"loggers": ["blinkpy"],
|
||||||
"requirements": ["blinkpy==0.25.2"]
|
"requirements": ["blinkpy==0.25.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,25 +13,32 @@ from bluecurrent_api.exceptions import (
|
|||||||
RequestLimitReached,
|
RequestLimitReached,
|
||||||
WebsocketError,
|
WebsocketError,
|
||||||
)
|
)
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
from homeassistant.const import CONF_API_TOKEN, Platform
|
from homeassistant.const import CONF_API_TOKEN, CONF_DEVICE_ID, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import (
|
||||||
from homeassistant.helpers import config_validation as cv
|
ConfigEntryAuthFailed,
|
||||||
|
ConfigEntryNotReady,
|
||||||
|
ServiceValidationError,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
BCU_APP,
|
||||||
CHARGEPOINT_SETTINGS,
|
CHARGEPOINT_SETTINGS,
|
||||||
CHARGEPOINT_STATUS,
|
CHARGEPOINT_STATUS,
|
||||||
|
CHARGING_CARD_ID,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVSE_ID,
|
EVSE_ID,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
PLUG_AND_CHARGE,
|
PLUG_AND_CHARGE,
|
||||||
|
SERVICE_START_CHARGE_SESSION,
|
||||||
VALUE,
|
VALUE,
|
||||||
)
|
)
|
||||||
from .services import async_setup_services
|
|
||||||
|
|
||||||
type BlueCurrentConfigEntry = ConfigEntry[Connector]
|
type BlueCurrentConfigEntry = ConfigEntry[Connector]
|
||||||
|
|
||||||
@@ -47,12 +54,13 @@ VALUE_TYPES = [CHARGEPOINT_STATUS, CHARGEPOINT_SETTINGS]
|
|||||||
|
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
SERVICE_START_CHARGE_SESSION_SCHEMA = vol.Schema(
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
{
|
||||||
"""Set up Blue Current."""
|
vol.Required(CONF_DEVICE_ID): cv.string,
|
||||||
|
# When no charging card is provided, use no charging card (BCU_APP = no charging card).
|
||||||
async_setup_services(hass)
|
vol.Optional(CHARGING_CARD_ID, default=BCU_APP): cv.string,
|
||||||
return True
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -80,6 +88,66 @@ async def async_setup_entry(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up Blue Current."""
|
||||||
|
|
||||||
|
async def start_charge_session(service_call: ServiceCall) -> None:
|
||||||
|
"""Start a charge session with the provided device and charge card ID."""
|
||||||
|
# When no charge card is provided, use the default charge card set in the config flow.
|
||||||
|
charging_card_id = service_call.data[CHARGING_CARD_ID]
|
||||||
|
device_id = service_call.data[CONF_DEVICE_ID]
|
||||||
|
|
||||||
|
# Get the device based on the given device ID.
|
||||||
|
device = dr.async_get(hass).devices.get(device_id)
|
||||||
|
|
||||||
|
if device is None:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN, translation_key="invalid_device_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
blue_current_config_entry: ConfigEntry | None = None
|
||||||
|
|
||||||
|
for config_entry_id in device.config_entries:
|
||||||
|
config_entry = hass.config_entries.async_get_entry(config_entry_id)
|
||||||
|
if not config_entry or config_entry.domain != DOMAIN:
|
||||||
|
# Not the blue_current config entry.
|
||||||
|
continue
|
||||||
|
|
||||||
|
if config_entry.state is not ConfigEntryState.LOADED:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN, translation_key="config_entry_not_loaded"
|
||||||
|
)
|
||||||
|
|
||||||
|
blue_current_config_entry = config_entry
|
||||||
|
break
|
||||||
|
|
||||||
|
if not blue_current_config_entry:
|
||||||
|
# The device is not connected to a valid blue_current config entry.
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN, translation_key="no_config_entry"
|
||||||
|
)
|
||||||
|
|
||||||
|
connector = blue_current_config_entry.runtime_data
|
||||||
|
|
||||||
|
# Get the evse_id from the identifier of the device.
|
||||||
|
evse_id = next(
|
||||||
|
identifier[1]
|
||||||
|
for identifier in device.identifiers
|
||||||
|
if identifier[0] == DOMAIN
|
||||||
|
)
|
||||||
|
|
||||||
|
await connector.client.start_session(evse_id, charging_card_id)
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_START_CHARGE_SESSION,
|
||||||
|
start_charge_session,
|
||||||
|
SERVICE_START_CHARGE_SESSION_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(
|
async def async_unload_entry(
|
||||||
hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
|
hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@gleeuwen", "@NickKoepr", "@jtodorova23"],
|
"codeowners": ["@gleeuwen", "@NickKoepr", "@jtodorova23"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/blue_current",
|
"documentation": "https://www.home-assistant.io/integrations/blue_current",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["bluecurrent_api"],
|
"loggers": ["bluecurrent_api"],
|
||||||
"requirements": ["bluecurrent-api==1.3.2"]
|
"requirements": ["bluecurrent-api==1.3.2"]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user